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