15
15
"""Helpers for :mod:`protobuf`."""
16
16
17
17
import collections
18
+ import copy
18
19
import inspect
19
20
20
- from google .protobuf .message import Message
21
+ from google .protobuf import field_mask_pb2
22
+ from google .protobuf import message
23
+ from google .protobuf import wrappers_pb2
21
24
22
25
_SENTINEL = object ()
26
+ _WRAPPER_TYPES = (
27
+ wrappers_pb2 .BoolValue ,
28
+ wrappers_pb2 .BytesValue ,
29
+ wrappers_pb2 .DoubleValue ,
30
+ wrappers_pb2 .FloatValue ,
31
+ wrappers_pb2 .Int32Value ,
32
+ wrappers_pb2 .Int64Value ,
33
+ wrappers_pb2 .StringValue ,
34
+ wrappers_pb2 .UInt32Value ,
35
+ wrappers_pb2 .UInt64Value ,
36
+ )
23
37
24
38
25
39
def from_any_pb (pb_type , any_pb ):
@@ -73,13 +87,15 @@ def get_messages(module):
73
87
module to find Message subclasses.
74
88
75
89
Returns:
76
- dict[str, Message]: A dictionary with the Message class names as
77
- keys, and the Message subclasses themselves as values.
90
+ dict[str, google.protobuf.message.Message]: A dictionary with the
91
+ Message class names as keys, and the Message subclasses themselves
92
+ as values.
78
93
"""
79
94
answer = collections .OrderedDict ()
80
95
for name in dir (module ):
81
96
candidate = getattr (module , name )
82
- if inspect .isclass (candidate ) and issubclass (candidate , Message ):
97
+ if (inspect .isclass (candidate ) and
98
+ issubclass (candidate , message .Message )):
83
99
answer [name ] = candidate
84
100
return answer
85
101
@@ -143,7 +159,7 @@ def get(msg_or_dict, key, default=_SENTINEL):
143
159
144
160
# Attempt to get the value from the two types of objects we know about.
145
161
# If we get something else, complain.
146
- if isinstance (msg_or_dict , Message ):
162
+ if isinstance (msg_or_dict , message . Message ):
147
163
answer = getattr (msg_or_dict , key , default )
148
164
elif isinstance (msg_or_dict , collections .Mapping ):
149
165
answer = msg_or_dict .get (key , default )
@@ -186,7 +202,7 @@ def _set_field_on_message(msg, key, value):
186
202
# Assign the dictionary values to the protobuf message.
187
203
for item_key , item_value in value .items ():
188
204
set (getattr (msg , key ), item_key , item_value )
189
- elif isinstance (value , Message ):
205
+ elif isinstance (value , message . Message ):
190
206
getattr (msg , key ).CopyFrom (value )
191
207
else :
192
208
setattr (msg , key , value )
@@ -205,7 +221,8 @@ def set(msg_or_dict, key, value):
205
221
TypeError: If ``msg_or_dict`` is not a Message or dictionary.
206
222
"""
207
223
# Sanity check: Is our target object valid?
208
- if not isinstance (msg_or_dict , (collections .MutableMapping , Message )):
224
+ if (not isinstance (msg_or_dict ,
225
+ (collections .MutableMapping , message .Message ))):
209
226
raise TypeError (
210
227
'set() expected a dict or protobuf message, got {!r}.' .format (
211
228
type (msg_or_dict )))
@@ -247,3 +264,84 @@ def setdefault(msg_or_dict, key, value):
247
264
"""
248
265
if not get (msg_or_dict , key , default = None ):
249
266
set (msg_or_dict , key , value )
267
+
268
+
269
+ def field_mask (original , modified ):
270
+ """Create a field mask by comparing two messages.
271
+
272
+ Args:
273
+ original (~google.protobuf.message.Message): the original message.
274
+ If set to None, this field will be interpretted as an empty
275
+ message.
276
+ modified (~google.protobuf.message.Message): the modified message.
277
+ If set to None, this field will be interpretted as an empty
278
+ message.
279
+
280
+ Returns:
281
+ google.protobuf.field_mask_pb2.FieldMask: field mask that contains
282
+ the list of field names that have different values between the two
283
+ messages. If the messages are equivalent, then the field mask is empty.
284
+
285
+ Raises:
286
+ ValueError: If the ``original`` or ``modified`` are not the same type.
287
+ """
288
+ if original is None and modified is None :
289
+ return field_mask_pb2 .FieldMask ()
290
+
291
+ if original is None and modified is not None :
292
+ original = copy .deepcopy (modified )
293
+ original .Clear ()
294
+
295
+ if modified is None and original is not None :
296
+ modified = copy .deepcopy (original )
297
+ modified .Clear ()
298
+
299
+ if type (original ) != type (modified ):
300
+ raise ValueError (
301
+ 'expected that both original and modified should be of the '
302
+ 'same type, received "{!r}" and "{!r}".' .
303
+ format (type (original ), type (modified )))
304
+
305
+ return field_mask_pb2 .FieldMask (
306
+ paths = _field_mask_helper (original , modified ))
307
+
308
+
309
+ def _field_mask_helper (original , modified , current = '' ):
310
+ answer = []
311
+
312
+ for name in original .DESCRIPTOR .fields_by_name :
313
+ field_path = _get_path (current , name )
314
+
315
+ original_val = getattr (original , name )
316
+ modified_val = getattr (modified , name )
317
+
318
+ if _is_message (original_val ) or _is_message (modified_val ):
319
+ if original_val != modified_val :
320
+ # Wrapper types do not need to include the .value part of the
321
+ # path.
322
+ if _is_wrapper (original_val ) or _is_wrapper (modified_val ):
323
+ answer .append (field_path )
324
+ elif not modified_val .ListFields ():
325
+ answer .append (field_path )
326
+ else :
327
+ answer .extend (_field_mask_helper (original_val ,
328
+ modified_val , field_path ))
329
+ else :
330
+ if original_val != modified_val :
331
+ answer .append (field_path )
332
+
333
+ return answer
334
+
335
+
336
+ def _get_path (current , name ):
337
+ if not current :
338
+ return name
339
+ return '%s.%s' % (current , name )
340
+
341
+
342
+ def _is_message (value ):
343
+ return isinstance (value , message .Message )
344
+
345
+
346
+ def _is_wrapper (value ):
347
+ return type (value ) in _WRAPPER_TYPES
0 commit comments