File: //snap/google-cloud-cli/394/lib/googlecloudsdk/api_lib/util/messages.py
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utilities for interacting with message classes and instances."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from apitools.base.protorpclite import messages as _messages
from apitools.base.py import encoding as _encoding
from googlecloudsdk.core import exceptions
import six
def UpdateMessage(message, diff):
"""Updates given message from diff object recursively.
The function recurses down through the properties of the diff object,
checking, for each key in the diff, if the equivalent property exists on the
message at the same depth. If the property exists, it is set to value from the
diff. If it does not exist, that diff key is silently ignored. All diff keys
are assumed to be strings.
Args:
message: An apitools.base.protorpclite.messages.Message instance.
diff: A dict of changes to apply to the message
e.g. {'settings': {'availabilityType': 'REGIONAL'}}.
Returns:
The modified message instance.
"""
if diff:
return _UpdateMessageHelper(message, diff)
return message
def _UpdateMessageHelper(message, diff):
for key, val in six.iteritems(diff):
if hasattr(message, key):
if isinstance(val, dict):
_UpdateMessageHelper(getattr(message, key), diff[key])
else:
setattr(message, key, val)
return message
class Error(exceptions.Error):
"""Indicates an error with an encoded protorpclite message."""
class DecodeError(Error):
"""Indicates an error in decoding a protorpclite message."""
@classmethod
def _FormatProtoPath(cls, edges, field_names):
"""Returns a string representation of a path to a proto field.
The return value represents one or more fields in a python dictionary
representation of a message (json/yaml) that could not be decoded into the
message as a string. The format is a dot separated list of python like sub
field references (name, name[index], name[name]). The final element of the
returned dot separated path may be a comma separated list of names enclosed
in curly braces to represent multiple subfields (see examples)
Examples:
o Reference to a single field that could not be decoded:
'a.b[1].c[x].d'
o Reference to two subfields
'a.b[1].c[x].{d,e}'
Args:
edges: List of objects representing python field references
(__str__ suitably defined.)
field_names: List of names for subfields of the message
that could not be decoded.
Returns:
A string representation of a python reference to a filed or
fields in a message that could not be decoded as described
above.
"""
# Format the edges.
path = [six.text_type(edge) for edge in edges]
if len(field_names) > 1:
# Use braces to group the errors when there are multiple errors.
# e.g. foo.bar.{x,y,z}
path.append('{{{}}}'.format(','.join(sorted(field_names))))
elif field_names:
# For single items, omit the braces.
# e.g. foo.bar.x
path.append(field_names[0])
return '.'.join(path)
@classmethod
def FromErrorPaths(cls, message, errors):
"""Returns a DecodeError from a list of locations of errors.
Args:
message: The protorpc Message in which a parsing error occurred.
errors: List[(edges, field_names)], A list of locations of errors
encountered while decoding the message.
"""
type_ = type(message).__name__
base_msg = 'Failed to parse value(s) in protobuf [{type_}]:'.format(
type_=type_)
error_paths = [' {type_}.{path}'.format(
type_=type_, path=cls._FormatProtoPath(edges, field_names))
for edges, field_names in errors]
return cls('\n'.join([base_msg] + error_paths))
class ScalarTypeMismatchError(DecodeError):
"""Incicates a scalar property was provided a value of an unexpected type."""
def DictToMessageWithErrorCheck(dict_,
message_type,
throw_on_unexpected_fields=True):
"""Convert "dict_" to a message of type message_type and check for errors.
A common use case is to define the dictionary by deserializing yaml or json.
Args:
dict_: The dict to parse into a protorpc Message.
message_type: The protorpc Message type.
throw_on_unexpected_fields: If this flag is set, an error will be raised if
the dictionary contains unrecognized fields.
Returns:
A message of type "message_type" parsed from "dict_".
Raises:
DecodeError: One or more unparsable values were found in the parsed message.
"""
try:
message = _encoding.DictToMessage(dict_, message_type)
except _messages.ValidationError as e:
# NOTE: The ValidationError message is passable but does not specify the
# full path to the property where the error occurred.
raise ScalarTypeMismatchError(
'Failed to parse value in protobuf [{type_}]:\n'
' {type_}.??: "{msg}"'.format(
type_=message_type.__name__, msg=six.text_type(e)))
except AttributeError:
# TODO(b/77547931): This is a bug in apitools and must be fixed upstream.
# The decode logic attempts an unchecked access to 'iteritems' assuming the
# Message field's associated value is a dict.
raise
else:
errors = list(_encoding.UnrecognizedFieldIter(message))
if errors and throw_on_unexpected_fields:
raise DecodeError.FromErrorPaths(message, errors)
return message