1919
2020from google .protobuf .message import Message
2121
22+ _SENTINEL = object ()
23+
2224
2325def from_any_pb (pb_type , any_pb ):
2426 """Converts an ``Any`` protobuf to the specified message type.
@@ -44,11 +46,13 @@ def from_any_pb(pb_type, any_pb):
4446
4547
4648def check_oneof (** kwargs ):
47- """Raise ValueError if more than one keyword argument is not none.
49+ """Raise ValueError if more than one keyword argument is not ``None``.
50+
4851 Args:
4952 kwargs (dict): The keyword arguments sent to the function.
53+
5054 Raises:
51- ValueError: If more than one entry in kwargs is not none .
55+ ValueError: If more than one entry in `` kwargs`` is not ``None`` .
5256 """
5357 # Sanity check: If no keyword arguments were sent, this is fine.
5458 if not kwargs :
@@ -62,10 +66,12 @@ def check_oneof(**kwargs):
6266
6367
6468def get_messages (module ):
65- """Return a dictionary of message names and objects.
69+ """Discovers all protobuf Message classes in a given import module.
70+
6671 Args:
67- module (module): A Python module; dir() will be run against this
72+ module (module): A Python module; :func:` dir` will be run against this
6873 module to find Message subclasses.
74+
6975 Returns:
7076 dict[str, Message]: A dictionary with the Message class names as
7177 keys, and the Message subclasses themselves as values.
@@ -76,3 +82,168 @@ def get_messages(module):
7682 if inspect .isclass (candidate ) and issubclass (candidate , Message ):
7783 answer [name ] = candidate
7884 return answer
85+
86+
87+ def _resolve_subkeys (key , separator = '.' ):
88+ """Resolve a potentially nested key.
89+
90+ If the key contains the ``separator`` (e.g. ``.``) then the key will be
91+ split on the first instance of the subkey::
92+
93+ >>> _resolve_subkeys('a.b.c')
94+ ('a', 'b.c')
95+ >>> _resolve_subkeys('d|e|f', separator='|')
96+ ('d', 'e|f')
97+
98+ If not, the subkey will be :data:`None`::
99+
100+ >>> _resolve_subkeys('foo')
101+ ('foo', None)
102+
103+ Args:
104+ key (str): A string that may or may not contain the separator.
105+ separator (str): The namespace separator. Defaults to `.`.
106+
107+ Returns:
108+ Tuple[str, str]: The key and subkey(s).
109+ """
110+ parts = key .split (separator , 1 )
111+
112+ if len (parts ) > 1 :
113+ return parts
114+ else :
115+ return parts [0 ], None
116+
117+
118+ def get (msg_or_dict , key , default = _SENTINEL ):
119+ """Retrieve a key's value from a protobuf Message or dictionary.
120+
121+ Args:
122+ mdg_or_dict (Union[~google.protobuf.message.Message, Mapping]): the
123+ object.
124+ key (str): The key to retrieve from the object.
125+ default (Any): If the key is not present on the object, and a default
126+ is set, returns that default instead. A type-appropriate falsy
127+ default is generally recommended, as protobuf messages almost
128+ always have default values for unset values and it is not always
129+ possible to tell the difference between a falsy value and an
130+ unset one. If no default is set then :class:`KeyError` will be
131+ raised if the key is not present in the object.
132+
133+ Returns:
134+ Any: The return value from the underlying Message or dict.
135+
136+ Raises:
137+ KeyError: If the key is not found. Note that, for unset values,
138+ messages and dictionaries may not have consistent behavior.
139+ TypeError: If ``msg_or_dict`` is not a Message or Mapping.
140+ """
141+ # We may need to get a nested key. Resolve this.
142+ key , subkey = _resolve_subkeys (key )
143+
144+ # Attempt to get the value from the two types of objects we know about.
145+ # If we get something else, complain.
146+ if isinstance (msg_or_dict , Message ):
147+ answer = getattr (msg_or_dict , key , default )
148+ elif isinstance (msg_or_dict , collections .Mapping ):
149+ answer = msg_or_dict .get (key , default )
150+ else :
151+ raise TypeError (
152+ 'get() expected a dict or protobuf message, got {!r}.' .format (
153+ type (msg_or_dict )))
154+
155+ # If the object we got back is our sentinel, raise KeyError; this is
156+ # a "not found" case.
157+ if answer is _SENTINEL :
158+ raise KeyError (key )
159+
160+ # If a subkey exists, call this method recursively against the answer.
161+ if subkey is not None and answer is not default :
162+ return get (answer , subkey , default = default )
163+
164+ return answer
165+
166+
167+ def _set_field_on_message (msg , key , value ):
168+ """Set helper for protobuf Messages."""
169+ # Attempt to set the value on the types of objects we know how to deal
170+ # with.
171+ if isinstance (value , (collections .MutableSequence , tuple )):
172+ # Clear the existing repeated protobuf message of any elements
173+ # currently inside it.
174+ while getattr (msg , key ):
175+ getattr (msg , key ).pop ()
176+
177+ # Write our new elements to the repeated field.
178+ for item in value :
179+ if isinstance (item , collections .Mapping ):
180+ getattr (msg , key ).add (** item )
181+ else :
182+ # protobuf's RepeatedCompositeContainer doesn't support
183+ # append.
184+ getattr (msg , key ).extend ([item ])
185+ elif isinstance (value , collections .Mapping ):
186+ # Assign the dictionary values to the protobuf message.
187+ for item_key , item_value in value .items ():
188+ set (getattr (msg , key ), item_key , item_value )
189+ elif isinstance (value , Message ):
190+ getattr (msg , key ).CopyFrom (value )
191+ else :
192+ setattr (msg , key , value )
193+
194+
195+ def set (msg_or_dict , key , value ):
196+ """Set a key's value on a protobuf Message or dictionary.
197+
198+ Args:
199+ msg_or_dict (Union[~google.protobuf.message.Message, Mapping]): the
200+ object.
201+ key (str): The key to set.
202+ value (Any): The value to set.
203+
204+ Raises:
205+ TypeError: If ``msg_or_dict`` is not a Message or dictionary.
206+ """
207+ # Sanity check: Is our target object valid?
208+ if not isinstance (msg_or_dict , (collections .MutableMapping , Message )):
209+ raise TypeError (
210+ 'set() expected a dict or protobuf message, got {!r}.' .format (
211+ type (msg_or_dict )))
212+
213+ # We may be setting a nested key. Resolve this.
214+ basekey , subkey = _resolve_subkeys (key )
215+
216+ # If a subkey exists, then get that object and call this method
217+ # recursively against it using the subkey.
218+ if subkey is not None :
219+ if isinstance (msg_or_dict , collections .MutableMapping ):
220+ msg_or_dict .setdefault (basekey , {})
221+ set (get (msg_or_dict , basekey ), subkey , value )
222+ return
223+
224+ if isinstance (msg_or_dict , collections .MutableMapping ):
225+ msg_or_dict [key ] = value
226+ else :
227+ _set_field_on_message (msg_or_dict , key , value )
228+
229+
230+ def setdefault (msg_or_dict , key , value ):
231+ """Set the key on a protobuf Message or dictionary to a given value if the
232+ current value is falsy.
233+
234+ Because protobuf Messages do not distinguish between unset values and
235+ falsy ones particularly well (by design), this method treats any falsy
236+ value (e.g. 0, empty list) as a target to be overwritten, on both Messages
237+ and dictionaries.
238+
239+ Args:
240+ msg_or_dict (Union[~google.protobuf.message.Message, Mapping]): the
241+ object.
242+ key (str): The key on the object in question.
243+ value (Any): The value to set.
244+
245+ Raises:
246+ TypeError: If ``msg_or_dict`` is not a Message or dictionary.
247+ """
248+ if not get (msg_or_dict , key , default = None ):
249+ set (msg_or_dict , key , value )
0 commit comments