1# -*- coding: utf-8 -*- # 2# Copyright 2018 Google LLC. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15"""Classes to define how concept args are added to argparse. 16 17A PresentationSpec is used to define how a concept spec is presented in an 18individual command, such as its help text. ResourcePresentationSpecs are 19used for resource specs. 20""" 21 22from __future__ import absolute_import 23from __future__ import division 24from __future__ import unicode_literals 25 26from googlecloudsdk.calliope.concepts import util 27from googlecloudsdk.command_lib.util.concepts import info_holders 28 29 30class PresentationSpec(object): 31 """Class that defines how concept arguments are presented in a command. 32 33 Attributes: 34 name: str, the name of the main arg for the concept. Can be positional or 35 flag style (UPPER_SNAKE_CASE or --lower-train-case). 36 concept_spec: googlecloudsdk.calliope.concepts.ConceptSpec, The spec that 37 specifies the concept. 38 group_help: str, the help text for the entire arg group. 39 prefixes: bool, whether to use prefixes before the attribute flags, such as 40 `--myresource-project`. 41 required: bool, whether the anchor argument should be required. If True, the 42 command will fail at argparse time if the anchor argument isn't given. 43 plural: bool, True if the resource will be parsed as a list, False 44 otherwise. 45 group: the parser or subparser for a Calliope command that the resource 46 arguments should be added to. If not provided, will be added to the main 47 parser. 48 attribute_to_args_map: {str: str}, dict of attribute names to names of 49 associated arguments. 50 """ 51 52 def __init__(self, name, concept_spec, group_help, prefixes=False, 53 required=False, flag_name_overrides=None, plural=False, 54 group=None): 55 """Initializes a ResourcePresentationSpec. 56 57 Args: 58 name: str, the name of the main arg for the concept. 59 concept_spec: googlecloudsdk.calliope.concepts.ConceptSpec, The spec that 60 specifies the concept. 61 group_help: str, the help text for the entire arg group. 62 prefixes: bool, whether to use prefixes before the attribute flags, such 63 as `--myresource-project`. This will match the "name" (in flag format). 64 required: bool, whether the anchor argument should be required. 65 flag_name_overrides: {str: str}, dict of attribute names to the desired 66 flag name. To remove a flag altogether, use '' as its rename value. 67 plural: bool, True if the resource will be parsed as a list, False 68 otherwise. 69 group: the parser or subparser for a Calliope command that the resource 70 arguments should be added to. If not provided, will be added to the main 71 parser. 72 """ 73 self.name = name 74 self._concept_spec = concept_spec 75 self.group_help = group_help 76 self.prefixes = prefixes 77 self.required = required 78 self.plural = plural 79 self.group = group 80 self._attribute_to_args_map = self._GetAttributeToArgsMap( 81 flag_name_overrides) 82 83 @property 84 def concept_spec(self): 85 """The ConceptSpec associated with the PresentationSpec. 86 87 Returns: 88 (googlecloudsdk.calliope.concepts.ConceptSpec) the concept spec. 89 """ 90 return self._concept_spec 91 92 @property 93 def attribute_to_args_map(self): 94 """The map of attribute names to associated args. 95 96 Returns: 97 {str: str}, the map. 98 """ 99 return self._attribute_to_args_map 100 101 def _GenerateInfo(self, fallthroughs_map): 102 """Generate a ConceptInfo object for the ConceptParser. 103 104 Must be overridden in subclasses. 105 106 Args: 107 fallthroughs_map: {str: [googlecloudsdk.calliope.concepts.deps. 108 _FallthroughBase]}, dict keyed by attribute name to lists of 109 fallthroughs. 110 111 Returns: 112 info_holders.ConceptInfo, the ConceptInfo object. 113 """ 114 raise NotImplementedError 115 116 def _GetAttributeToArgsMap(self, flag_name_overrides): 117 """Generate a map of attributes to primary arg names. 118 119 Must be overridden in subclasses. 120 121 Args: 122 flag_name_overrides: {str: str}, the dict of flags to overridden names. 123 124 Returns: 125 {str: str}, dict from attribute names to arg names. 126 """ 127 raise NotImplementedError 128 129 130class ResourcePresentationSpec(PresentationSpec): 131 """Class that specifies how resource arguments are presented in a command.""" 132 133 def _ValidateFlagNameOverrides(self, flag_name_overrides): 134 if not flag_name_overrides: 135 return 136 for attribute_name in flag_name_overrides.keys(): 137 for attribute in self.concept_spec.attributes: 138 if attribute.name == attribute_name: 139 break 140 else: 141 raise ValueError( 142 'Attempting to override the name for an attribute not present in ' 143 'the concept: [{}]. Available attributes: [{}]'.format( 144 attribute_name, 145 ', '.join([attribute.name 146 for attribute in self.concept_spec.attributes]))) 147 148 def _GetAttributeToArgsMap(self, flag_name_overrides): 149 self._ValidateFlagNameOverrides(flag_name_overrides) 150 # Create a rename map for the attributes to their flags. 151 attribute_to_args_map = {} 152 for i, attribute in enumerate(self._concept_spec.attributes): 153 is_anchor = i == len(self._concept_spec.attributes) - 1 154 name = self.GetFlagName( 155 attribute.name, self.name, flag_name_overrides, self.prefixes, 156 is_anchor=is_anchor) 157 if name: 158 attribute_to_args_map[attribute.name] = name 159 return attribute_to_args_map 160 161 @staticmethod 162 def GetFlagName(attribute_name, presentation_name, flag_name_overrides=None, 163 prefixes=False, is_anchor=False): 164 """Gets the flag name for a given attribute name. 165 166 Returns a flag name for an attribute, adding prefixes as necessary or using 167 overrides if an override map is provided. 168 169 Args: 170 attribute_name: str, the name of the attribute to base the flag name on. 171 presentation_name: str, the anchor argument name of the resource the 172 attribute belongs to (e.g. '--foo'). 173 flag_name_overrides: {str: str}, a dict of attribute names to exact string 174 of the flag name to use for the attribute. None if no overrides. 175 prefixes: bool, whether to use the resource name as a prefix for the flag. 176 is_anchor: bool, True if this it he anchor flag, False otherwise. 177 178 Returns: 179 (str) the name of the flag. 180 """ 181 flag_name_overrides = flag_name_overrides or {} 182 if attribute_name in flag_name_overrides: 183 return flag_name_overrides.get(attribute_name) 184 if attribute_name == 'project': 185 return '' 186 if is_anchor: 187 return presentation_name 188 prefix = util.PREFIX 189 if prefixes: 190 if presentation_name.startswith(util.PREFIX): 191 prefix += presentation_name[len(util.PREFIX):] + '-' 192 else: 193 prefix += presentation_name.lower().replace('_', '-') + '-' 194 return prefix + attribute_name 195 196 def _GenerateInfo(self, fallthroughs_map): 197 """Gets the ResourceInfo object for the ConceptParser. 198 199 Args: 200 fallthroughs_map: {str: [googlecloudsdk.calliope.concepts.deps. 201 _FallthroughBase]}, dict keyed by attribute name to lists of 202 fallthroughs. 203 204 Returns: 205 info_holders.ResourceInfo, the ResourceInfo object. 206 """ 207 return info_holders.ResourceInfo( 208 self.name, 209 self.concept_spec, 210 self.group_help, 211 self.attribute_to_args_map, 212 fallthroughs_map, 213 required=self.required, 214 plural=self.plural, 215 group=self.group) 216 217 def __eq__(self, other): 218 if not isinstance(other, type(self)): 219 return False 220 return (self.name == other.name and 221 self.concept_spec == other.concept_spec and 222 self.group_help == other.group_help and 223 self.prefixes == other.prefixes and 224 self.plural == other.plural and 225 self.required == other.required and 226 self.group == other.group) 227 228 229# Currently no other type of multitype concepts have been implemented. 230class MultitypeResourcePresentationSpec(PresentationSpec): 231 """A resource-specific presentation spec.""" 232 233 def _GetAttributeToArgsMap(self, flag_name_overrides): 234 # Create a rename map for the attributes to their flags. 235 attribute_to_args_map = {} 236 leaf_anchors = [a for a in self._concept_spec.attributes 237 if self._concept_spec.IsLeafAnchor(a)] 238 for attribute in self._concept_spec.attributes: 239 is_anchor = [attribute] == leaf_anchors 240 name = self.GetFlagName( 241 attribute.name, self.name, flag_name_overrides=flag_name_overrides, 242 prefixes=self.prefixes, is_anchor=is_anchor) 243 if name: 244 attribute_to_args_map[attribute.name] = name 245 return attribute_to_args_map 246 247 @staticmethod 248 def GetFlagName(attribute_name, presentation_name, flag_name_overrides=None, 249 prefixes=False, is_anchor=False): 250 """Gets the flag name for a given attribute name. 251 252 Returns a flag name for an attribute, adding prefixes as necessary or using 253 overrides if an override map is provided. 254 255 Args: 256 attribute_name: str, the name of the attribute to base the flag name on. 257 presentation_name: str, the anchor argument name of the resource the 258 attribute belongs to (e.g. '--foo'). 259 flag_name_overrides: {str: str}, a dict of attribute names to exact string 260 of the flag name to use for the attribute. None if no overrides. 261 prefixes: bool, whether to use the resource name as a prefix for the flag. 262 is_anchor: bool, True if this is the anchor flag, False otherwise. 263 264 Returns: 265 (str) the name of the flag. 266 """ 267 flag_name_overrides = flag_name_overrides or {} 268 if attribute_name in flag_name_overrides: 269 return flag_name_overrides.get(attribute_name) 270 if is_anchor: 271 return presentation_name 272 if attribute_name == 'project': 273 return '' 274 275 if prefixes: 276 return util.FlagNameFormat('-'.join([presentation_name, attribute_name])) 277 return util.FlagNameFormat(attribute_name) 278 279 def _GenerateInfo(self, fallthroughs_map): 280 """Gets the MultitypeResourceInfo object for the ConceptParser. 281 282 Args: 283 fallthroughs_map: {str: [googlecloudsdk.calliope.concepts.deps. 284 _FallthroughBase]}, dict keyed by attribute name to lists of 285 fallthroughs. 286 287 Returns: 288 info_holders.MultitypeResourceInfo, the ResourceInfo object. 289 """ 290 return info_holders.MultitypeResourceInfo( 291 self.name, 292 self.concept_spec, 293 self.group_help, 294 self.attribute_to_args_map, 295 fallthroughs_map, 296 required=self.required, 297 plural=self.plural, 298 group=self.group) 299 300 def __eq__(self, other): 301 if not isinstance(other, type(self)): 302 return False 303 return (self.name == other.name and 304 self.concept_spec == other.concept_spec and 305 self.group_help == other.group_help and 306 self.prefixes == other.prefixes and 307 self.plural == other.plural and 308 self.required == other.required and 309 self.group == other.group) 310