1#!/usr/bin/env python 2"""Common code for converting proto to other formats, such as JSON.""" 3 4import base64 5import collections 6import datetime 7import json 8import logging 9import os 10import sys 11 12from protorpc import message_types 13from protorpc import messages 14from protorpc import protojson 15import six 16 17from apitools.base.py import exceptions 18 19__all__ = [ 20 'CopyProtoMessage', 21 'JsonToMessage', 22 'MessageToJson', 23 'DictToMessage', 24 'MessageToDict', 25 'PyValueToMessage', 26 'MessageToPyValue', 27 'MessageToRepr', 28 'GetCustomJsonFieldMapping', 29 'AddCustomJsonFieldMapping', 30 'GetCustomJsonEnumMapping', 31 'AddCustomJsonEnumMapping', 32] 33 34 35_Codec = collections.namedtuple('_Codec', ['encoder', 'decoder']) 36CodecResult = collections.namedtuple('CodecResult', ['value', 'complete']) 37 38 39# TODO(craigcitro): Make these non-global. 40_UNRECOGNIZED_FIELD_MAPPINGS = {} 41_CUSTOM_MESSAGE_CODECS = {} 42_CUSTOM_FIELD_CODECS = {} 43_FIELD_TYPE_CODECS = {} 44 45 46def MapUnrecognizedFields(field_name): 47 """Register field_name as a container for unrecognized fields.""" 48 def Register(cls): 49 _UNRECOGNIZED_FIELD_MAPPINGS[cls] = field_name 50 return cls 51 return Register 52 53 54def RegisterCustomMessageCodec(encoder, decoder): 55 """Register a custom encoder/decoder for this message class.""" 56 def Register(cls): 57 _CUSTOM_MESSAGE_CODECS[cls] = _Codec(encoder=encoder, decoder=decoder) 58 return cls 59 return Register 60 61 62def RegisterCustomFieldCodec(encoder, decoder): 63 """Register a custom encoder/decoder for this field.""" 64 def Register(field): 65 _CUSTOM_FIELD_CODECS[field] = _Codec(encoder=encoder, decoder=decoder) 66 return field 67 return Register 68 69 70def RegisterFieldTypeCodec(encoder, decoder): 71 """Register a custom encoder/decoder for all fields of this type.""" 72 def Register(field_type): 73 _FIELD_TYPE_CODECS[field_type] = _Codec( 74 encoder=encoder, decoder=decoder) 75 return field_type 76 return Register 77 78 79# TODO(craigcitro): Delete this function with the switch to proto2. 80def CopyProtoMessage(message): 81 codec = protojson.ProtoJson() 82 return codec.decode_message(type(message), codec.encode_message(message)) 83 84 85def MessageToJson(message, include_fields=None): 86 """Convert the given message to JSON.""" 87 result = _ProtoJsonApiTools.Get().encode_message(message) 88 return _IncludeFields(result, message, include_fields) 89 90 91def JsonToMessage(message_type, message): 92 """Convert the given JSON to a message of type message_type.""" 93 return _ProtoJsonApiTools.Get().decode_message(message_type, message) 94 95 96# TODO(craigcitro): Do this directly, instead of via JSON. 97def DictToMessage(d, message_type): 98 """Convert the given dictionary to a message of type message_type.""" 99 return JsonToMessage(message_type, json.dumps(d)) 100 101 102def MessageToDict(message): 103 """Convert the given message to a dictionary.""" 104 return json.loads(MessageToJson(message)) 105 106 107def PyValueToMessage(message_type, value): 108 """Convert the given python value to a message of type message_type.""" 109 return JsonToMessage(message_type, json.dumps(value)) 110 111 112def MessageToPyValue(message): 113 """Convert the given message to a python value.""" 114 return json.loads(MessageToJson(message)) 115 116 117def MessageToRepr(msg, multiline=False, **kwargs): 118 """Return a repr-style string for a protorpc message. 119 120 protorpc.Message.__repr__ does not return anything that could be considered 121 python code. Adding this function lets us print a protorpc message in such 122 a way that it could be pasted into code later, and used to compare against 123 other things. 124 125 Args: 126 msg: protorpc.Message, the message to be repr'd. 127 multiline: bool, True if the returned string should have each field 128 assignment on its own line. 129 **kwargs: {str:str}, Additional flags for how to format the string. 130 131 Known **kwargs: 132 shortstrings: bool, True if all string values should be 133 truncated at 100 characters, since when mocking the contents 134 typically don't matter except for IDs, and IDs are usually 135 less than 100 characters. 136 no_modules: bool, True if the long module name should not be printed with 137 each type. 138 139 Returns: 140 str, A string of valid python (assuming the right imports have been made) 141 that recreates the message passed into this function. 142 143 """ 144 145 # TODO(user): craigcitro suggests a pretty-printer from apitools/gen. 146 147 indent = kwargs.get('indent', 0) 148 149 def IndentKwargs(kwargs): 150 kwargs = dict(kwargs) 151 kwargs['indent'] = kwargs.get('indent', 0) + 4 152 return kwargs 153 154 if isinstance(msg, list): 155 s = '[' 156 for item in msg: 157 if multiline: 158 s += '\n' + ' ' * (indent + 4) 159 s += MessageToRepr( 160 item, multiline=multiline, **IndentKwargs(kwargs)) + ',' 161 if multiline: 162 s += '\n' + ' ' * indent 163 s += ']' 164 return s 165 166 if isinstance(msg, messages.Message): 167 s = type(msg).__name__ + '(' 168 if not kwargs.get('no_modules'): 169 s = msg.__module__ + '.' + s 170 names = sorted([field.name for field in msg.all_fields()]) 171 for name in names: 172 field = msg.field_by_name(name) 173 if multiline: 174 s += '\n' + ' ' * (indent + 4) 175 value = getattr(msg, field.name) 176 s += field.name + '=' + MessageToRepr( 177 value, multiline=multiline, **IndentKwargs(kwargs)) + ',' 178 if multiline: 179 s += '\n' + ' ' * indent 180 s += ')' 181 return s 182 183 if isinstance(msg, six.string_types): 184 if kwargs.get('shortstrings') and len(msg) > 100: 185 msg = msg[:100] 186 187 if isinstance(msg, datetime.datetime): 188 189 class SpecialTZInfo(datetime.tzinfo): 190 191 def __init__(self, offset): 192 super(SpecialTZInfo, self).__init__() 193 self.offset = offset 194 195 def __repr__(self): 196 s = 'TimeZoneOffset(' + repr(self.offset) + ')' 197 if not kwargs.get('no_modules'): 198 s = 'protorpc.util.' + s 199 return s 200 201 msg = datetime.datetime( 202 msg.year, msg.month, msg.day, msg.hour, msg.minute, msg.second, 203 msg.microsecond, SpecialTZInfo(msg.tzinfo.utcoffset(0))) 204 205 return repr(msg) 206 207 208def _GetField(message, field_path): 209 for field in field_path: 210 if field not in dir(message): 211 raise KeyError('no field "%s"' % field) 212 message = getattr(message, field) 213 return message 214 215 216def _SetField(dictblob, field_path, value): 217 for field in field_path[:-1]: 218 dictblob[field] = {} 219 dictblob = dictblob[field] 220 dictblob[field_path[-1]] = value 221 222 223def _IncludeFields(encoded_message, message, include_fields): 224 """Add the requested fields to the encoded message.""" 225 if include_fields is None: 226 return encoded_message 227 result = json.loads(encoded_message) 228 for field_name in include_fields: 229 try: 230 value = _GetField(message, field_name.split('.')) 231 nullvalue = None 232 if isinstance(value, list): 233 nullvalue = [] 234 except KeyError: 235 raise exceptions.InvalidDataError( 236 'No field named %s in message of type %s' % ( 237 field_name, type(message))) 238 _SetField(result, field_name.split('.'), nullvalue) 239 return json.dumps(result) 240 241 242def _GetFieldCodecs(field, attr): 243 result = [ 244 getattr(_CUSTOM_FIELD_CODECS.get(field), attr, None), 245 getattr(_FIELD_TYPE_CODECS.get(type(field)), attr, None), 246 ] 247 return [x for x in result if x is not None] 248 249 250class _ProtoJsonApiTools(protojson.ProtoJson): 251 252 """JSON encoder used by apitools clients.""" 253 _INSTANCE = None 254 255 @classmethod 256 def Get(cls): 257 if cls._INSTANCE is None: 258 cls._INSTANCE = cls() 259 return cls._INSTANCE 260 261 def decode_message(self, message_type, encoded_message): 262 if message_type in _CUSTOM_MESSAGE_CODECS: 263 return _CUSTOM_MESSAGE_CODECS[ 264 message_type].decoder(encoded_message) 265 # We turn off the default logging in protorpc. We may want to 266 # remove this later. 267 old_level = logging.getLogger().level 268 logging.getLogger().setLevel(logging.ERROR) 269 try: 270 result = _DecodeCustomFieldNames(message_type, encoded_message) 271 result = super(_ProtoJsonApiTools, self).decode_message( 272 message_type, result) 273 finally: 274 logging.getLogger().setLevel(old_level) 275 result = _ProcessUnknownEnums(result, encoded_message) 276 result = _ProcessUnknownMessages(result, encoded_message) 277 return _DecodeUnknownFields(result, encoded_message) 278 279 def decode_field(self, field, value): 280 """Decode the given JSON value. 281 282 Args: 283 field: a messages.Field for the field we're decoding. 284 value: a python value we'd like to decode. 285 286 Returns: 287 A value suitable for assignment to field. 288 """ 289 for decoder in _GetFieldCodecs(field, 'decoder'): 290 result = decoder(field, value) 291 value = result.value 292 if result.complete: 293 return value 294 if isinstance(field, messages.MessageField): 295 field_value = self.decode_message( 296 field.message_type, json.dumps(value)) 297 elif isinstance(field, messages.EnumField): 298 value = GetCustomJsonEnumMapping( 299 field.type, json_name=value) or value 300 try: 301 field_value = super( 302 _ProtoJsonApiTools, self).decode_field(field, value) 303 except messages.DecodeError: 304 if not isinstance(value, six.string_types): 305 raise 306 field_value = None 307 else: 308 field_value = super( 309 _ProtoJsonApiTools, self).decode_field(field, value) 310 return field_value 311 312 def encode_message(self, message): 313 if isinstance(message, messages.FieldList): 314 return '[%s]' % (', '.join(self.encode_message(x) 315 for x in message)) 316 message_type = type(message) 317 if message_type in _CUSTOM_MESSAGE_CODECS: 318 return _CUSTOM_MESSAGE_CODECS[type(message)].encoder(message) 319 message = _EncodeUnknownFields(message) 320 result = super(_ProtoJsonApiTools, self).encode_message(message) 321 return _EncodeCustomFieldNames(message, result) 322 323 def encode_field(self, field, value): 324 """Encode the given value as JSON. 325 326 Args: 327 field: a messages.Field for the field we're encoding. 328 value: a value for field. 329 330 Returns: 331 A python value suitable for json.dumps. 332 """ 333 for encoder in _GetFieldCodecs(field, 'encoder'): 334 result = encoder(field, value) 335 value = result.value 336 if result.complete: 337 return value 338 if isinstance(field, messages.EnumField): 339 if field.repeated: 340 remapped_value = [GetCustomJsonEnumMapping( 341 field.type, python_name=e.name) or e.name for e in value] 342 else: 343 remapped_value = GetCustomJsonEnumMapping( 344 field.type, python_name=value.name) 345 if remapped_value: 346 return remapped_value 347 if (isinstance(field, messages.MessageField) and 348 not isinstance(field, message_types.DateTimeField)): 349 value = json.loads(self.encode_message(value)) 350 return super(_ProtoJsonApiTools, self).encode_field(field, value) 351 352 353# TODO(craigcitro): Fold this and _IncludeFields in as codecs. 354def _DecodeUnknownFields(message, encoded_message): 355 """Rewrite unknown fields in message into message.destination.""" 356 destination = _UNRECOGNIZED_FIELD_MAPPINGS.get(type(message)) 357 if destination is None: 358 return message 359 pair_field = message.field_by_name(destination) 360 if not isinstance(pair_field, messages.MessageField): 361 raise exceptions.InvalidDataFromServerError( 362 'Unrecognized fields must be mapped to a compound ' 363 'message type.') 364 pair_type = pair_field.message_type 365 # TODO(craigcitro): Add more error checking around the pair 366 # type being exactly what we suspect (field names, etc). 367 if isinstance(pair_type.value, messages.MessageField): 368 new_values = _DecodeUnknownMessages( 369 message, json.loads(encoded_message), pair_type) 370 else: 371 new_values = _DecodeUnrecognizedFields(message, pair_type) 372 setattr(message, destination, new_values) 373 # We could probably get away with not setting this, but 374 # why not clear it? 375 setattr(message, '_Message__unrecognized_fields', {}) 376 return message 377 378 379def _DecodeUnknownMessages(message, encoded_message, pair_type): 380 """Process unknown fields in encoded_message of a message type.""" 381 field_type = pair_type.value.type 382 new_values = [] 383 all_field_names = [x.name for x in message.all_fields()] 384 for name, value_dict in encoded_message.items(): 385 if name in all_field_names: 386 continue 387 value = PyValueToMessage(field_type, value_dict) 388 if pair_type.value.repeated: 389 value = _AsMessageList(value) 390 new_pair = pair_type(key=name, value=value) 391 new_values.append(new_pair) 392 return new_values 393 394 395def _DecodeUnrecognizedFields(message, pair_type): 396 """Process unrecognized fields in message.""" 397 new_values = [] 398 for unknown_field in message.all_unrecognized_fields(): 399 # TODO(craigcitro): Consider validating the variant if 400 # the assignment below doesn't take care of it. It may 401 # also be necessary to check it in the case that the 402 # type has multiple encodings. 403 value, _ = message.get_unrecognized_field_info(unknown_field) 404 value_type = pair_type.field_by_name('value') 405 if isinstance(value_type, messages.MessageField): 406 decoded_value = DictToMessage(value, pair_type.value.message_type) 407 elif isinstance(value_type, messages.EnumField): 408 decoded_value = pair_type.value.type(value) 409 else: 410 decoded_value = value 411 new_pair = pair_type(key=str(unknown_field), value=decoded_value) 412 new_values.append(new_pair) 413 return new_values 414 415 416def _EncodeUnknownFields(message): 417 """Remap unknown fields in message out of message.source.""" 418 source = _UNRECOGNIZED_FIELD_MAPPINGS.get(type(message)) 419 if source is None: 420 return message 421 result = CopyProtoMessage(message) 422 pairs_field = message.field_by_name(source) 423 if not isinstance(pairs_field, messages.MessageField): 424 raise exceptions.InvalidUserInputError( 425 'Invalid pairs field %s' % pairs_field) 426 pairs_type = pairs_field.message_type 427 value_variant = pairs_type.field_by_name('value').variant 428 pairs = getattr(message, source) 429 for pair in pairs: 430 if value_variant == messages.Variant.MESSAGE: 431 encoded_value = MessageToDict(pair.value) 432 else: 433 encoded_value = pair.value 434 result.set_unrecognized_field(pair.key, encoded_value, value_variant) 435 setattr(result, source, []) 436 return result 437 438 439def _SafeEncodeBytes(field, value): 440 """Encode the bytes in value as urlsafe base64.""" 441 try: 442 if field.repeated: 443 result = [base64.urlsafe_b64encode(byte) for byte in value] 444 else: 445 result = base64.urlsafe_b64encode(value) 446 complete = True 447 except TypeError: 448 result = value 449 complete = False 450 return CodecResult(value=result, complete=complete) 451 452 453def _SafeDecodeBytes(unused_field, value): 454 """Decode the urlsafe base64 value into bytes.""" 455 try: 456 result = base64.urlsafe_b64decode(str(value)) 457 complete = True 458 except TypeError: 459 result = value 460 complete = False 461 return CodecResult(value=result, complete=complete) 462 463 464def _ProcessUnknownEnums(message, encoded_message): 465 """Add unknown enum values from encoded_message as unknown fields. 466 467 ProtoRPC diverges from the usual protocol buffer behavior here and 468 doesn't allow unknown fields. Throwing on unknown fields makes it 469 impossible to let servers add new enum values and stay compatible 470 with older clients, which isn't reasonable for us. We simply store 471 unrecognized enum values as unknown fields, and all is well. 472 473 Args: 474 message: Proto message we've decoded thus far. 475 encoded_message: JSON string we're decoding. 476 477 Returns: 478 message, with any unknown enums stored as unrecognized fields. 479 """ 480 if not encoded_message: 481 return message 482 decoded_message = json.loads(encoded_message) 483 for field in message.all_fields(): 484 if (isinstance(field, messages.EnumField) and 485 field.name in decoded_message and 486 message.get_assigned_value(field.name) is None): 487 message.set_unrecognized_field( 488 field.name, decoded_message[field.name], messages.Variant.ENUM) 489 return message 490 491 492def _ProcessUnknownMessages(message, encoded_message): 493 """Store any remaining unknown fields as strings. 494 495 ProtoRPC currently ignores unknown values for which no type can be 496 determined (and logs a "No variant found" message). For the purposes 497 of reserializing, this is quite harmful (since it throws away 498 information). Here we simply add those as unknown fields of type 499 string (so that they can easily be reserialized). 500 501 Args: 502 message: Proto message we've decoded thus far. 503 encoded_message: JSON string we're decoding. 504 505 Returns: 506 message, with any remaining unrecognized fields saved. 507 """ 508 if not encoded_message: 509 return message 510 decoded_message = json.loads(encoded_message) 511 message_fields = [x.name for x in message.all_fields()] + list( 512 message.all_unrecognized_fields()) 513 missing_fields = [x for x in decoded_message.keys() 514 if x not in message_fields] 515 for field_name in missing_fields: 516 message.set_unrecognized_field(field_name, decoded_message[field_name], 517 messages.Variant.STRING) 518 return message 519 520 521RegisterFieldTypeCodec(_SafeEncodeBytes, _SafeDecodeBytes)(messages.BytesField) 522 523 524# Note that these could share a dictionary, since they're keyed by 525# distinct types, but it's not really worth it. 526_JSON_ENUM_MAPPINGS = {} 527_JSON_FIELD_MAPPINGS = {} 528 529 530def _GetTypeKey(message_type, package): 531 """Get the prefix for this message type in mapping dicts.""" 532 key = message_type.definition_name() 533 if package and key.startswith(package + '.'): 534 module_name = message_type.__module__ 535 # We normalize '__main__' to something unique, if possible. 536 if module_name == '__main__': 537 try: 538 file_name = sys.modules[module_name].__file__ 539 except (AttributeError, KeyError): 540 pass 541 else: 542 base_name = os.path.basename(file_name) 543 split_name = os.path.splitext(base_name) 544 if len(split_name) == 1: 545 module_name = unicode(base_name) 546 else: 547 module_name = u'.'.join(split_name[:-1]) 548 key = module_name + '.' + key.partition('.')[2] 549 return key 550 551 552def AddCustomJsonEnumMapping(enum_type, python_name, json_name, 553 package=''): 554 """Add a custom wire encoding for a given enum value. 555 556 This is primarily used in generated code, to handle enum values 557 which happen to be Python keywords. 558 559 Args: 560 enum_type: (messages.Enum) An enum type 561 python_name: (string) Python name for this value. 562 json_name: (string) JSON name to be used on the wire. 563 package: (basestring, optional) Package prefix for this enum, if 564 present. We strip this off the enum name in order to generate 565 unique keys. 566 """ 567 if not issubclass(enum_type, messages.Enum): 568 raise exceptions.TypecheckError( 569 'Cannot set JSON enum mapping for non-enum "%s"' % enum_type) 570 enum_name = _GetTypeKey(enum_type, package) 571 if python_name not in enum_type.names(): 572 raise exceptions.InvalidDataError( 573 'Enum value %s not a value for type %s' % (python_name, enum_type)) 574 field_mappings = _JSON_ENUM_MAPPINGS.setdefault(enum_name, {}) 575 _CheckForExistingMappings('enum', enum_type, python_name, json_name) 576 field_mappings[python_name] = json_name 577 578 579def AddCustomJsonFieldMapping(message_type, python_name, json_name, 580 package=''): 581 """Add a custom wire encoding for a given message field. 582 583 This is primarily used in generated code, to handle enum values 584 which happen to be Python keywords. 585 586 Args: 587 message_type: (messages.Message) A message type 588 python_name: (string) Python name for this value. 589 json_name: (string) JSON name to be used on the wire. 590 package: (basestring, optional) Package prefix for this message, if 591 present. We strip this off the message name in order to generate 592 unique keys. 593 """ 594 if not issubclass(message_type, messages.Message): 595 raise exceptions.TypecheckError( 596 'Cannot set JSON field mapping for ' 597 'non-message "%s"' % message_type) 598 message_name = _GetTypeKey(message_type, package) 599 try: 600 _ = message_type.field_by_name(python_name) 601 except KeyError: 602 raise exceptions.InvalidDataError( 603 'Field %s not recognized for type %s' % ( 604 python_name, message_type)) 605 field_mappings = _JSON_FIELD_MAPPINGS.setdefault(message_name, {}) 606 _CheckForExistingMappings('field', message_type, python_name, json_name) 607 field_mappings[python_name] = json_name 608 609 610def GetCustomJsonEnumMapping(enum_type, python_name=None, json_name=None): 611 """Return the appropriate remapping for the given enum, or None.""" 612 return _FetchRemapping(enum_type.definition_name(), 'enum', 613 python_name=python_name, json_name=json_name, 614 mappings=_JSON_ENUM_MAPPINGS) 615 616 617def GetCustomJsonFieldMapping(message_type, python_name=None, json_name=None): 618 """Return the appropriate remapping for the given field, or None.""" 619 return _FetchRemapping(message_type.definition_name(), 'field', 620 python_name=python_name, json_name=json_name, 621 mappings=_JSON_FIELD_MAPPINGS) 622 623 624def _FetchRemapping(type_name, mapping_type, python_name=None, json_name=None, 625 mappings=None): 626 """Common code for fetching a key or value from a remapping dict.""" 627 if python_name and json_name: 628 raise exceptions.InvalidDataError( 629 'Cannot specify both python_name and json_name ' 630 'for %s remapping' % mapping_type) 631 if not (python_name or json_name): 632 raise exceptions.InvalidDataError( 633 'Must specify either python_name or json_name for %s remapping' % ( 634 mapping_type,)) 635 field_remappings = mappings.get(type_name, {}) 636 if field_remappings: 637 if python_name: 638 return field_remappings.get(python_name) 639 elif json_name: 640 if json_name in list(field_remappings.values()): 641 return [k for k in field_remappings 642 if field_remappings[k] == json_name][0] 643 return None 644 645 646def _CheckForExistingMappings(mapping_type, message_type, 647 python_name, json_name): 648 """Validate that no mappings exist for the given values.""" 649 if mapping_type == 'field': 650 getter = GetCustomJsonFieldMapping 651 elif mapping_type == 'enum': 652 getter = GetCustomJsonEnumMapping 653 remapping = getter(message_type, python_name=python_name) 654 if remapping is not None: 655 raise exceptions.InvalidDataError( 656 'Cannot add mapping for %s "%s", already mapped to "%s"' % ( 657 mapping_type, python_name, remapping)) 658 remapping = getter(message_type, json_name=json_name) 659 if remapping is not None: 660 raise exceptions.InvalidDataError( 661 'Cannot add mapping for %s "%s", already mapped to "%s"' % ( 662 mapping_type, json_name, remapping)) 663 664 665def _EncodeCustomFieldNames(message, encoded_value): 666 message_name = type(message).definition_name() 667 field_remappings = list(_JSON_FIELD_MAPPINGS.get(message_name, {}).items()) 668 if field_remappings: 669 decoded_value = json.loads(encoded_value) 670 for python_name, json_name in field_remappings: 671 if python_name in encoded_value: 672 decoded_value[json_name] = decoded_value.pop(python_name) 673 encoded_value = json.dumps(decoded_value) 674 return encoded_value 675 676 677def _DecodeCustomFieldNames(message_type, encoded_message): 678 message_name = message_type.definition_name() 679 field_remappings = _JSON_FIELD_MAPPINGS.get(message_name, {}) 680 if field_remappings: 681 decoded_message = json.loads(encoded_message) 682 for python_name, json_name in list(field_remappings.items()): 683 if json_name in decoded_message: 684 decoded_message[python_name] = decoded_message.pop(json_name) 685 encoded_message = json.dumps(decoded_message) 686 return encoded_message 687 688 689def _AsMessageList(msg): 690 """Convert the provided list-as-JsonValue to a list.""" 691 # This really needs to live in extra_types, but extra_types needs 692 # to import this file to be able to register codecs. 693 # TODO(craigcitro): Split out a codecs module and fix this ugly 694 # import. 695 from apitools.base.py import extra_types 696 697 def _IsRepeatedJsonValue(msg): 698 """Return True if msg is a repeated value as a JsonValue.""" 699 if isinstance(msg, extra_types.JsonArray): 700 return True 701 if isinstance(msg, extra_types.JsonValue) and msg.array_value: 702 return True 703 return False 704 705 if not _IsRepeatedJsonValue(msg): 706 raise ValueError('invalid argument to _AsMessageList') 707 if isinstance(msg, extra_types.JsonValue): 708 msg = msg.array_value 709 if isinstance(msg, extra_types.JsonArray): 710 msg = msg.entries 711 return msg 712