19
19
20
20
from google .protobuf .message import Message
21
21
22
+ _SENTINEL = object ()
23
+
22
24
23
25
def from_any_pb (pb_type , any_pb ):
24
26
"""Converts an ``Any`` protobuf to the specified message type.
@@ -44,11 +46,13 @@ def from_any_pb(pb_type, any_pb):
44
46
45
47
46
48
def 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
+
48
51
Args:
49
52
kwargs (dict): The keyword arguments sent to the function.
53
+
50
54
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`` .
52
56
"""
53
57
# Sanity check: If no keyword arguments were sent, this is fine.
54
58
if not kwargs :
@@ -62,10 +66,12 @@ def check_oneof(**kwargs):
62
66
63
67
64
68
def get_messages (module ):
65
- """Return a dictionary of message names and objects.
69
+ """Discovers all protobuf Message classes in a given import module.
70
+
66
71
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
68
73
module to find Message subclasses.
74
+
69
75
Returns:
70
76
dict[str, Message]: A dictionary with the Message class names as
71
77
keys, and the Message subclasses themselves as values.
@@ -76,3 +82,168 @@ def get_messages(module):
76
82
if inspect .isclass (candidate ) and issubclass (candidate , Message ):
77
83
answer [name ] = candidate
78
84
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