1# -*- coding: utf-8 -*- # 2# Copyright 2017 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 16"""Utilities for generating and parsing arguments from API fields.""" 17 18from __future__ import absolute_import 19from __future__ import division 20from __future__ import unicode_literals 21 22from collections import OrderedDict 23import re 24 25from apitools.base.protorpclite import messages 26from apitools.base.py import encoding 27from googlecloudsdk.calliope import arg_parsers 28from googlecloudsdk.calliope import base 29from googlecloudsdk.core import properties 30from googlecloudsdk.core.resource import resource_property 31from googlecloudsdk.core.util import http_encoding 32 33import six 34 35# Used to determine if a value has been set for an argument 36UNSPECIFIED = object() 37 38 39class Error(Exception): 40 """Base exception for this module.""" 41 pass 42 43 44class UnknownFieldError(Error): 45 """The referenced field could not be found in the message object.""" 46 47 def __init__(self, field_name, message): 48 super(UnknownFieldError, self).__init__( 49 'Field [{}] not found in message [{}]. Available fields: [{}]' 50 .format(field_name, message.__class__.__name__, 51 ', '.join(f.name for f in message.all_fields()))) 52 53 54class InvalidFieldPathError(Error): 55 """The referenced field path could not be found in the message object.""" 56 57 def __init__(self, field_path, message, reason): 58 super(InvalidFieldPathError, self).__init__( 59 'Invalid field path [{}] for message [{}]. Details: [{}]' 60 .format(field_path, message.__class__.__name__, reason)) 61 62 63class ArgumentGenerationError(Error): 64 """Generic error when we can't auto generate an argument for an api field.""" 65 66 def __init__(self, field_name, reason): 67 super(ArgumentGenerationError, self).__init__( 68 'Failed to generate argument for field [{}]: {}' 69 .format(field_name, reason)) 70 71 72def GetFieldFromMessage(message, field_path): 73 """Extract the field object from the message using a dotted field path. 74 75 If the field does not exist, an error is logged. 76 77 Args: 78 message: The apitools message to dig into. 79 field_path: str, The dotted path of attributes and sub-attributes. 80 81 Returns: 82 The Field object. 83 """ 84 fields = field_path.split('.') 85 for f in fields[:-1]: 86 message = _GetField(message, f).type 87 return _GetField(message, fields[-1]) 88 89 90def GetFieldValueFromMessage(message, field_path): 91 """Extract the value of the field given a dotted field path. 92 93 If the field_path does not exist, an error is logged. 94 95 Args: 96 message: The apitools message to dig into. 97 field_path: str, The dotted path of attributes and sub-attributes. 98 99 Raises: 100 InvalidFieldPathError: When the path is invalid. 101 102 Returns: 103 The value or if not set, None. 104 """ 105 root_message = message 106 fields = field_path.split('.') 107 for i, f in enumerate(fields): 108 index_found = re.match(r'(.+)\[(\d+)\]$', f) 109 if index_found: 110 # Split field path segment (e.g. abc[1]) into abc and 1. 111 f, index = index_found.groups() 112 index = int(index) 113 114 try: 115 field = message.field_by_name(f) 116 except KeyError: 117 raise InvalidFieldPathError(field_path, root_message, 118 UnknownFieldError(f, message)) 119 if index_found: 120 if not field.repeated: 121 raise InvalidFieldPathError( 122 field_path, root_message, 123 'Index cannot be specified for non-repeated field [{}]'.format(f)) 124 else: 125 if field.repeated and i < len(fields) - 1: 126 raise InvalidFieldPathError( 127 field_path, root_message, 128 'Index needs to be specified for repeated field [{}]'.format(f)) 129 130 message = getattr(message, f) 131 if message and index_found: 132 message = message[index] if index < len(message) else None 133 134 if not message and i < len(fields) - 1: 135 if isinstance(field, messages.MessageField): 136 # Create an instance of the message so we can continue down the path, to 137 # verify if the path is valid. 138 message = field.type() 139 else: 140 raise InvalidFieldPathError( 141 field_path, root_message, 142 '[{}] is not a valid field on field [{}]' 143 .format(f, field.type.__name__)) 144 145 return message 146 147 148def SetFieldInMessage(message, field_path, value): 149 """Sets the given field in the message object. 150 151 Args: 152 message: A constructed apitools message object to inject the value into. 153 field_path: str, The dotted path of attributes and sub-attributes. 154 value: The value to set. 155 """ 156 fields = field_path.split('.') 157 for f in fields[:-1]: 158 sub_message = getattr(message, f) 159 is_repeated = _GetField(message, f).repeated 160 if not sub_message: 161 sub_message = _GetField(message, f).type() 162 if is_repeated: 163 sub_message = [sub_message] 164 setattr(message, f, sub_message) 165 message = sub_message[0] if is_repeated else sub_message 166 field_type = _GetField(message, fields[-1]).type 167 if isinstance(value, dict): 168 value = encoding.PyValueToMessage(field_type, value) 169 if isinstance(value, list): 170 for i, item in enumerate(value): 171 if isinstance(item, dict): 172 value[i] = encoding.PyValueToMessage(field_type, item) 173 setattr(message, fields[-1], value) 174 175 176def _GetField(message, field_name): 177 try: 178 return message.field_by_name(field_name) 179 except KeyError: 180 raise UnknownFieldError(field_name, message) 181 182 183# TODO(b/64147277): Pass this down from the generator, don't hard code. 184DEFAULT_PARAMS = {'project': properties.VALUES.core.project.Get, 185 'projectId': properties.VALUES.core.project.Get, 186 'projectsId': properties.VALUES.core.project.Get, 187 } 188 189 190def GetFromNamespace(namespace, arg_name, fallback=None, use_defaults=False): 191 """Gets the given argument from the namespace.""" 192 if arg_name.startswith('--'): 193 arg_name = arg_name[2:] 194 normalized_arg_name = arg_name.replace('-', '_') 195 value = getattr(namespace, normalized_arg_name, None) 196 if not value and fallback: 197 value = fallback() 198 if not value and use_defaults: 199 value = DEFAULT_PARAMS.get(arg_name, lambda: None)() 200 return value 201 202 203def Limit(method, namespace): 204 """Gets the value of the limit flag (if present).""" 205 if (hasattr(namespace, 'limit') and method.IsPageableList() and 206 method.ListItemField()): 207 return getattr(namespace, 'limit') 208 209 210def PageSize(method, namespace): 211 """Gets the value of the page size flag (if present).""" 212 if (hasattr(namespace, 'page_size') and method.IsPageableList() and 213 method.ListItemField() and method.BatchPageSizeField()): 214 return getattr(namespace, 'page_size') 215 216 217class RepeatedMessageBindableType(object): 218 """An interface for custom type generators that bind directly to a message. 219 220 An argparse type function converts the parsed string into an object. Some 221 types (like ArgDicts) can only be generated once we know what message it will 222 be bound to (because the spec of the ArgDict depends on the fields and types 223 in the message. This interface allows encapsulating the logic to generate a 224 type function at the point when the message it is being bound to is known. 225 """ 226 227 def GenerateType(self, message): 228 """Generates an argparse type function to use to parse the argument. 229 230 Args: 231 message: The apitools message class. 232 """ 233 pass 234 235 def Action(self): 236 """The argparse action to use for this argument. 237 238 'store' is the default action, but sometimes something like 'append' might 239 be required to allow the argument to be repeated and all values collected. 240 241 Returns: 242 str, The argparse action to use. 243 """ 244 return 'store' 245 246 247def GenerateFlag(field, attributes, fix_bools=True, category=None): 248 """Generates a flag for a single field in a message. 249 250 Args: 251 field: The apitools field object. 252 attributes: yaml_command_schema.Argument, The attributes to use to 253 generate the arg. 254 fix_bools: True to generate boolean flags as switches that take a value or 255 False to just generate them as regular string flags. 256 category: The help category to put the flag in. 257 258 Raises: 259 ArgumentGenerationError: When an argument could not be generated from the 260 API field. 261 262 Returns: 263 calliope.base.Argument, The generated argument. 264 """ 265 variant = field.variant if field else None 266 t = attributes.type or TYPES.get(variant, None) 267 268 choices = None 269 if attributes.choices is not None: 270 choice_map = {c.arg_value: c.help_text for c in attributes.choices} 271 # If help text is provided, give a choice map. Otherwise, just use the 272 # choice values. 273 choices = (choice_map if any(choice_map.values()) 274 else sorted(choice_map.keys())) 275 elif variant == messages.Variant.ENUM: 276 choices = [EnumNameToChoice(name) for name in sorted(field.type.names())] 277 278 action = attributes.action 279 if t == bool and fix_bools and not action: 280 # For boolean flags, we want to create a flag with action 'store_true' 281 # rather than a flag that takes a value and converts it to a boolean. Only 282 # do this if not using a custom action. 283 action = 'store_true' 284 # Default action is store if one was not provided. 285 action = action or 'store' 286 287 # pylint: disable=g-explicit-bool-comparison, only an explicit False should 288 # override this, None just means to do the default. 289 repeated = (field and field.repeated) and attributes.repeated != False 290 291 if repeated: 292 if action != 'store': 293 raise ArgumentGenerationError( 294 field.name, 295 'The field is repeated but is using a custom action. You might' 296 ' want to set repeated: False in your arg spec.') 297 if t: 298 # A special ArgDict wrapper type was given, bind it to the message so it 299 # can generate the message from the key/value pairs. 300 if isinstance(t, RepeatedMessageBindableType): 301 action = t.Action() 302 t = t.GenerateType(field.type) 303 # If a simple type was provided, just use a list of that type (even if it 304 # is a message). The type function will be responsible for converting to 305 # the correct value. If type is an ArgList or ArgDict, don't try to wrap 306 # it. 307 elif not isinstance(t, arg_parsers.ArgList): 308 t = arg_parsers.ArgList(element_type=t, choices=choices) 309 # Don't register the choices on the argparse arg because it is validated 310 # by the ArgList. 311 choices = None 312 elif isinstance(t, RepeatedMessageBindableType): 313 raise ArgumentGenerationError( 314 field.name, 'The given type can only be used on repeated fields.') 315 316 if field and not t and action == 'store' and not attributes.processor: 317 # The type is unknown and there is no custom action or processor, we don't 318 # know what to do with this. 319 raise ArgumentGenerationError( 320 field.name, 'The field is of an unknown type. You can specify a type ' 321 'function or a processor to manually handle this argument.') 322 323 name = attributes.arg_name 324 arg = base.Argument( 325 name if attributes.is_positional else '--' + name, 326 category=category if not attributes.is_positional else None, 327 action=action, 328 completer=attributes.completer, 329 help=attributes.help_text, 330 hidden=attributes.hidden, 331 ) 332 if attributes.default != UNSPECIFIED: 333 arg.kwargs['default'] = attributes.default 334 if action != 'store_true': 335 # For this special action type, it won't accept a bunch of the common 336 # kwargs, so we can only add them if not generating a boolean flag. 337 metavar = attributes.metavar or name 338 arg.kwargs['metavar'] = resource_property.ConvertToAngrySnakeCase( 339 metavar.replace('-', '_')) 340 arg.kwargs['type'] = t 341 arg.kwargs['choices'] = choices 342 if not attributes.is_positional: 343 arg.kwargs['required'] = attributes.required 344 return arg 345 346 347def ConvertValue(field, value, repeated=None, processor=None, choices=None): 348 """Coverts the parsed value into something to insert into a request message. 349 350 If a processor is registered, that is called on the value. 351 If a choices mapping was provided, each value is mapped back into its original 352 value. 353 If the field is an enum, the value will be looked up by name and the Enum type 354 constructed. 355 356 Args: 357 field: The apitools field object. 358 value: The parsed value. This must be a scalar for scalar fields and a list 359 for repeated fields. 360 repeated: bool, Set to False if this arg was forced to be singular even 361 though the API field it corresponds to is repeated. 362 processor: A function to process the value before putting it into the 363 message. 364 choices: {str: str} A mapping of argument value, to enum API enum value. 365 366 Returns: 367 The value to insert into the message. 368 """ 369 # pylint: disable=g-explicit-bool-comparison, only an explicit False should 370 # override this, None just means to do the default. 371 arg_repeated = field.repeated and repeated != False 372 373 if processor: 374 value = processor(value) 375 else: 376 valid_choices = None 377 if choices: 378 valid_choices = choices.keys() 379 if field.variant == messages.Variant.ENUM: 380 api_names = field.type.names() 381 else: 382 api_names = [] 383 CheckValidEnumNames(api_names, choices.values()) 384 if arg_repeated: 385 value = [_MapChoice(choices, v) for v in value] 386 else: 387 value = _MapChoice(choices, value) 388 if field.variant == messages.Variant.ENUM: 389 t = field.type 390 if arg_repeated: 391 value = [ChoiceToEnum(v, t, valid_choices=valid_choices) for v in value] 392 else: 393 value = ChoiceToEnum(value, t, valid_choices=valid_choices) 394 395 if field.repeated and not arg_repeated and not isinstance(value, list): 396 # If we manually made this arg singular, but it is actually a repeated field 397 # wrap it in a list. 398 value = [value] 399 return value 400 401 402def _MapChoice(choices, value): 403 if isinstance(value, six.string_types): 404 value = value.lower() 405 return choices.get(value, value) 406 407 408def ParseResourceIntoMessage(ref, method, message, resource_method_params=None, 409 request_id_field=None, use_relative_name=True): 410 """Set fields in message corresponding to a resource. 411 412 Args: 413 ref: googlecloudsdk.core.resources.Resource, the resource reference. 414 method: the API method. 415 message: apitools Message object. 416 resource_method_params: {str: str}, A mapping of API method parameter name 417 to resource ref attribute name, if any 418 request_id_field: str, the name that the ID of the resource arg takes if the 419 API method params and the resource params don't match. 420 use_relative_name: Used ref.RelativeName() if True, otherwise ref.Name(). 421 """ 422 resource_method_params = resource_method_params or {} 423 resource_method_params = resource_method_params.copy() 424 425 # This only happens for non-list methods where the API method params don't 426 # match the resource parameters (basically only create methods). In this 427 # case, we re-parse the resource as its parent collection (to fill in the 428 # API parameters, and we insert the name of the resource itself into the 429 # correct position in the body of the request method. 430 if (request_id_field and method.resource_argument_collection.detailed_params 431 != method.request_collection.detailed_params): 432 # Sets the name of the resource in the message object body. 433 SetFieldInMessage(message, request_id_field, ref.Name()) 434 # Create a reference for the parent resource to put in the API params. 435 ref = ref.Parent( 436 parent_collection=method.request_collection.full_name) 437 438 ref_name = ref.RelativeName() if use_relative_name else ref.Name() 439 for p in method.params: 440 value = getattr(ref, resource_method_params.pop(p, p), ref_name) 441 SetFieldInMessage(message, p, value) 442 for message_field_name, ref_param_name in resource_method_params.items(): 443 value = getattr(ref, ref_param_name, ref_name) 444 SetFieldInMessage(message, message_field_name, value) 445 446 447def ParseStaticFieldsIntoMessage(message, static_fields=None): 448 """Set fields in message corresponding to a dict of static field values. 449 450 Args: 451 message: the Apitools message. 452 static_fields: dict of fields to values. 453 """ 454 static_fields = static_fields or {} 455 for field_path, value in six.iteritems(static_fields): 456 field = GetFieldFromMessage(message, field_path) 457 SetFieldInMessage( 458 message, field_path, ConvertValue(field, value)) 459 460 461def ParseExistingMessageIntoMessage(message, existing_message, method): 462 """Sets fields in message based on an existing message. 463 464 This function is used for get-modify-update pattern. The request type of 465 update requests would be either the same as the response type of get requests 466 or one field inside the request would be the same as the get response. 467 468 For example: 469 1) update.request_type_name = ServiceAccount 470 get.response_type_name = ServiceAccount 471 2) update.request_type_name = updateInstanceRequest 472 updateInstanceRequest.instance = Instance 473 get.response_type_name = Instance 474 475 If the existing message has the same type as the message to be sent for the 476 request, then return the existing message instead. If they are different, find 477 the field in the message which has the same type as existing_message, then 478 assign exsiting message to that field. 479 480 Args: 481 message: the apitools message to construct a new request. 482 existing_message: the exsting apitools message returned from server. 483 method: APIMethod, the method to generate request for. 484 485 Returns: 486 A modified apitools message to be send to the method. 487 """ 488 if type(existing_message) == type(message): # pylint: disable=unidiomatic-typecheck 489 return existing_message 490 491 # For read-modify-update API calls, the field to modify will exist either in 492 # the request message itself, or in a nested message one level below the 493 # request. Assume at first that it exists in the request message itself: 494 field_path = method.request_field 495 field = message.field_by_name(method.request_field) 496 # If this is not the case, then the field must be nested one level below. 497 if field.message_type != type(existing_message): 498 # We don't know what the name of the field is in the nested message, so we 499 # look through all of them until we find one with the right type. 500 nested_message = field.message_type() 501 for nested_field in nested_message.all_fields(): 502 try: 503 if nested_field.message_type == type(existing_message): 504 field_path += '.' + nested_field.name 505 break 506 except AttributeError: # Ignore non-message fields. 507 pass 508 509 SetFieldInMessage(message, field_path, existing_message) 510 return message 511 512 513def CheckValidEnumNames(api_names, choices_values): 514 """Ensures the api_name given in the spec matches a value from the API.""" 515 if api_names: 516 bad_choices = [name for name in choices_values if not ( 517 name in api_names or ChoiceToEnumName( 518 six.text_type(name)) in api_names)] 519 else: 520 bad_choices = [] 521 if bad_choices: 522 raise arg_parsers.ArgumentTypeError( 523 '{} is/are not valid enum values.'.format(', '.join(bad_choices))) 524 525 526def ChoiceToEnum(choice, enum_type, item_type='choice', valid_choices=None): 527 """Converts the typed choice into an apitools Enum value.""" 528 if choice is None: 529 return None 530 name = ChoiceToEnumName(choice) 531 valid_choices = (valid_choices or 532 [EnumNameToChoice(n) for n in enum_type.names()]) 533 try: 534 return enum_type.lookup_by_name(name) 535 except KeyError: 536 raise arg_parsers.ArgumentTypeError( 537 'Invalid {item}: {selection}. Valid choices are: [{values}].'.format( 538 item=item_type, 539 selection=EnumNameToChoice(name), 540 values=', '.join(c for c in sorted(valid_choices)))) 541 542 543def ChoiceToEnumName(choice): 544 """Converts a typeable choice to the string representation of the Enum.""" 545 return choice.replace('-', '_').upper() 546 547 548def EnumNameToChoice(name): 549 """Converts the name of an Enum value into a typeable choice.""" 550 return name.replace('_', '-').lower() 551 552 553_LONG_TYPE = long if six.PY2 else int 554 555 556TYPES = { 557 messages.Variant.DOUBLE: float, 558 messages.Variant.FLOAT: float, 559 560 messages.Variant.INT64: _LONG_TYPE, 561 messages.Variant.UINT64: _LONG_TYPE, 562 messages.Variant.SINT64: _LONG_TYPE, 563 564 messages.Variant.INT32: int, 565 messages.Variant.UINT32: int, 566 messages.Variant.SINT32: int, 567 568 messages.Variant.STRING: six.text_type, 569 messages.Variant.BOOL: bool, 570 571 # TODO(b/70980549): Do something better with bytes. 572 messages.Variant.BYTES: http_encoding.Encode, 573 # For enums, we want to accept upper and lower case from the user, but 574 # always compare against lowercase enum choices. 575 messages.Variant.ENUM: EnumNameToChoice, 576 messages.Variant.MESSAGE: None, 577} 578 579 580def FieldHelpDocs(message, section='Fields'): 581 """Gets the help text for the fields in the request message. 582 583 Args: 584 message: The apitools message. 585 section: str, The section to extract help data from. Fields is the default, 586 may also be Values to extract enum data, for example. 587 588 Returns: 589 {str: str}, A mapping of field name to help text. 590 """ 591 field_helps = {} 592 current_field = None 593 594 match = re.search(r'^\s+{}:.*$'.format(section), 595 message.__doc__ or '', re.MULTILINE) 596 if not match: 597 # Couldn't find any fields at all. 598 return field_helps 599 600 for line in message.__doc__[match.end():].splitlines(): 601 match = re.match(r'^\s+(\w+): (.*)$', line) 602 if match: 603 # This line is the start of a new field. 604 current_field = match.group(1) 605 field_helps[current_field] = match.group(2).strip() 606 elif current_field: 607 # Append additional text to the in progress field. 608 to_append = line.strip() 609 if to_append: 610 current_text = field_helps.get(current_field, '') 611 field_helps[current_field] = current_text + ' ' + to_append 612 613 return field_helps 614 615 616def GetRecursiveMessageSpec(message, definitions=None): 617 """Gets the recursive representation of a message as a dictionary. 618 619 Args: 620 message: The apitools message. 621 definitions: A list of message definitions already encountered. 622 623 Returns: 624 {str: object}, A recursive mapping of field name to its data. 625 """ 626 if definitions is None: 627 definitions = [] 628 if message in definitions: 629 # This message has already been seen along this path, 630 # don't recursive (forever). 631 return {} 632 definitions.append(message) 633 field_helps = FieldHelpDocs(message) 634 data = {} 635 for field in message.all_fields(): 636 field_data = {'description': field_helps.get(field.name)} 637 field_data['repeated'] = field.repeated 638 if field.variant == messages.Variant.MESSAGE: 639 field_data['type'] = field.type.__name__ 640 fields = GetRecursiveMessageSpec(field.type, definitions=definitions) 641 if fields: 642 field_data['fields'] = fields 643 else: 644 field_data['type'] = field.variant 645 if field.variant == messages.Variant.ENUM: 646 enum_help = FieldHelpDocs(field.type, 'Values') 647 field_data['choices'] = {n: enum_help.get(n) 648 for n in field.type.names()} 649 650 data[field.name] = field_data 651 definitions.pop() 652 return data 653 654 655def IsOutputField(help_text): 656 """Determines if the given field is output only based on help text.""" 657 return help_text and ( 658 help_text.startswith('[Output Only]') or 659 help_text.endswith('@OutputOnly')) 660 661 662class ChoiceEnumMapper(object): 663 """Utility class for mapping apitools Enum messages to argparse choice args. 664 665 Dynamically builds a base.Argument from an enum message. 666 Derives choice values from supplied enum or an optional custom_mapping dict 667 (see below). 668 669 Class Attributes: 670 choices: Either a list of strings [str] specifying the commandline choice 671 values or an ordered dict of choice value to choice help string mappings 672 {str -> str} 673 enum: underlying enum whos values map to supplied choices. 674 choice_arg: base.Argument object 675 choice_mappings: Mapping of argparse choice value strings to enum values. 676 custom_mappings: Optional dict mapping enum values to a custom 677 argparse choice value. To maintain compatiblity with base.ChoiceAgrument(), 678 dict can be either: 679 {str-> str} - Enum String value to choice argument value i.e. 680 {'MY_MUCH_LONGER_ENUM_VALUE':'short-arg'} 681 OR 682 {str -> (str, str)} - Enum string value to tuple of 683 (choice argument value, choice help string) i.e. 684 {'MY_MUCH_LONGER_ENUM_VALUE':('short-arg','My short arg help text.')} 685 """ 686 _CUSTOM_MAPPING_ERROR = ('custom_mappings must be a dict of enum string ' 687 'values to argparse argument choices. Choices must ' 688 'be either a string or a string tuple of (choice, ' 689 'choice_help_text): [{}]') 690 691 def __init__(self, 692 arg_name, 693 message_enum, 694 custom_mappings=None, 695 help_str=None, 696 required=False, 697 action=None, 698 metavar=None, 699 dest=None, 700 default=None, 701 hidden=False, 702 include_filter=None): 703 """Initialize ChoiceEnumMapper. 704 705 Args: 706 arg_name: str, The name of the argparse argument to create 707 message_enum: apitools.Enum, the enum to map 708 custom_mappings: See Above. 709 help_str: string, pass through for base.Argument, 710 see base.ChoiceArgument(). 711 required: boolean,string, pass through for base.Argument, 712 see base.ChoiceArgument(). 713 action: string or argparse.Action, string, pass through for base.Argument, 714 see base.ChoiceArgument(). 715 metavar: string, string, pass through for base.Argument, 716 see base.ChoiceArgument().. 717 dest: string, string, pass through for base.Argument, 718 see base.ChoiceArgument(). 719 default: string, string, pass through for base.Argument, 720 see base.ChoiceArgument(). 721 hidden: boolean, pass through for base.Argument, 722 see base.ChoiceArgument(). 723 include_filter: callable, function of type string->bool used to filter 724 enum values from message_enum that should be included in choices. 725 If include_filter returns True for a particular enum value, it will be 726 included otherwise it will be excluded. This is ignored if 727 custom_mappings is specified. 728 729 Raises: 730 ValueError: If no enum is given, mappings are incomplete 731 TypeError: If invalid values are passed for base.Argument or 732 custom_mapping 733 """ 734 # pylint:disable=protected-access 735 if not isinstance(message_enum, messages._EnumClass): 736 raise ValueError('Invalid Message Enum: [{}]'.format(message_enum)) 737 self._arg_name = arg_name 738 self._enum = message_enum 739 self._custom_mappings = custom_mappings 740 if include_filter is not None and not callable(include_filter): 741 raise TypeError('include_filter must be callable received [{}]'.format( 742 include_filter)) 743 744 self._filter = include_filter 745 self._filtered_enum = self._enum 746 self._ValidateAndParseMappings() 747 self._choice_arg = base.ChoiceArgument( 748 arg_name, 749 self.choices, 750 help_str=help_str, 751 required=required, 752 action=action, 753 metavar=metavar, 754 dest=dest, 755 default=default, 756 hidden=hidden) 757 758 def _ValidateAndParseMappings(self): 759 """Validates and parses choice to enum mappings. 760 761 Validates and parses choice to enum mappings including any custom mappings. 762 763 Raises: 764 ValueError: custom_mappings does not contain correct number of mapped 765 values. 766 TypeError: custom_mappings is incorrect type or contains incorrect types 767 for mapped values. 768 """ 769 if self._custom_mappings: # Process Custom Mappings 770 if not isinstance(self._custom_mappings, dict): 771 raise TypeError( 772 self._CUSTOM_MAPPING_ERROR.format(self._custom_mappings)) 773 enum_strings = set([x.name for x in self._enum]) 774 diff = set(self._custom_mappings.keys()) - enum_strings 775 if diff: 776 raise ValueError('custom_mappings [{}] may only contain mappings' 777 ' for enum values. invalid values:[{}]'.format( 778 ', '.join(self._custom_mappings.keys()), 779 ', '.join(diff))) 780 try: 781 self._ParseCustomMappingsFromTuples() 782 except (TypeError, ValueError): 783 self._ParseCustomMappingsFromStrings() 784 785 else: # No Custom Mappings so do automagic mapping 786 if callable(self._filter): 787 self._filtered_enum = [ 788 e for e in self._enum if self._filter(e.name) 789 ] 790 791 self._choice_to_enum = { 792 EnumNameToChoice(x.name): x 793 for x in self._filtered_enum 794 } 795 self._enum_to_choice = { 796 y.name: x 797 for x, y in six.iteritems(self._choice_to_enum) 798 } 799 self._choices = sorted(self._choice_to_enum.keys()) 800 801 def _ParseCustomMappingsFromTuples(self): 802 """Parses choice to enum mappings from custom_mapping with tuples. 803 804 Parses choice mappings from dict mapping Enum strings to a tuple of 805 choice values and choice help {str -> (str, str)} mapping. 806 807 Raises: 808 TypeError - Custom choices are not not valid (str,str) tuples. 809 """ 810 self._choice_to_enum = {} 811 self._enum_to_choice = {} 812 self._choices = OrderedDict() 813 for enum_string, (choice, help_str) in sorted( 814 six.iteritems(self._custom_mappings)): 815 self._choice_to_enum[choice] = self._enum(enum_string) 816 self._enum_to_choice[enum_string] = choice 817 self._choices[choice] = help_str 818 819 def _ParseCustomMappingsFromStrings(self): 820 """Parses choice to enum mappings from custom_mapping with strings. 821 822 Parses choice mappings from dict mapping Enum strings to choice 823 values {str -> str} mapping. 824 825 Raises: 826 TypeError - Custom choices are not strings 827 """ 828 self._choice_to_enum = {} 829 self._choices = [] 830 831 for enum_string, choice_string in sorted( 832 six.iteritems(self._custom_mappings)): 833 if not isinstance(choice_string, six.string_types): 834 raise TypeError( 835 self._CUSTOM_MAPPING_ERROR.format(self._custom_mappings)) 836 self._choice_to_enum[choice_string] = self._enum(enum_string) 837 self._choices.append(choice_string) 838 self._enum_to_choice = self._custom_mappings 839 840 def GetChoiceForEnum(self, enum_value): 841 """Converts an enum value to a choice argument value.""" 842 return self._enum_to_choice.get(six.text_type(enum_value)) 843 844 def GetEnumForChoice(self, choice_value): 845 """Converts a mapped string choice value to an enum.""" 846 return self._choice_to_enum.get(choice_value) 847 848 @property 849 def choices(self): 850 return self._choices 851 852 @property 853 def enum(self): 854 return self._enum 855 856 @property 857 def filtered_enum(self): 858 return self._filtered_enum 859 860 @property 861 def choice_arg(self): 862 return self._choice_arg 863 864 @property 865 def choice_mappings(self): 866 return self._choice_to_enum 867 868 @property 869 def custom_mappings(self): 870 return self._custom_mappings 871 872 @property 873 def include_filter(self): 874 return self._filter 875