1# Protocol Buffers - Google's data interchange format
2# Copyright 2008 Google Inc. All rights reserved.
3#
4# Use of this source code is governed by a BSD-style
5# license that can be found in the LICENSE file or at
6# https://developers.google.com/open-source/licenses/bsd
7"""Contains FieldMask class."""
8
9from google.protobuf.descriptor import FieldDescriptor
10
11
12class FieldMask(object):
13 """Class for FieldMask message type."""
14
15 __slots__ = ()
16
17 def ToJsonString(self):
18 """Converts FieldMask to string according to ProtoJSON spec."""
19 camelcase_paths = []
20 for path in self.paths:
21 camelcase_paths.append(_SnakeCaseToCamelCase(path))
22 return ','.join(camelcase_paths)
23
24 def FromJsonString(self, value):
25 """Converts string to FieldMask according to ProtoJSON spec."""
26 if not isinstance(value, str):
27 raise ValueError('FieldMask JSON value not a string: {!r}'.format(value))
28 self.Clear()
29 if value:
30 for path in value.split(','):
31 self.paths.append(_CamelCaseToSnakeCase(path))
32
33 def IsValidForDescriptor(self, message_descriptor):
34 """Checks whether the FieldMask is valid for Message Descriptor."""
35 for path in self.paths:
36 if not _IsValidPath(message_descriptor, path):
37 return False
38 return True
39
40 def AllFieldsFromDescriptor(self, message_descriptor):
41 """Gets all direct fields of Message Descriptor to FieldMask."""
42 self.Clear()
43 for field in message_descriptor.fields:
44 self.paths.append(field.name)
45
46 def CanonicalFormFromMask(self, mask):
47 """Converts a FieldMask to the canonical form.
48
49 Removes paths that are covered by another path. For example,
50 "foo.bar" is covered by "foo" and will be removed if "foo"
51 is also in the FieldMask. Then sorts all paths in alphabetical order.
52
53 Args:
54 mask: The original FieldMask to be converted.
55 """
56 tree = _FieldMaskTree(mask)
57 tree.ToFieldMask(self)
58
59 def Union(self, mask1, mask2):
60 """Merges mask1 and mask2 into this FieldMask."""
61 _CheckFieldMaskMessage(mask1)
62 _CheckFieldMaskMessage(mask2)
63 tree = _FieldMaskTree(mask1)
64 tree.MergeFromFieldMask(mask2)
65 tree.ToFieldMask(self)
66
67 def Intersect(self, mask1, mask2):
68 """Intersects mask1 and mask2 into this FieldMask."""
69 _CheckFieldMaskMessage(mask1)
70 _CheckFieldMaskMessage(mask2)
71 tree = _FieldMaskTree(mask1)
72 intersection = _FieldMaskTree()
73 for path in mask2.paths:
74 tree.IntersectPath(path, intersection)
75 intersection.ToFieldMask(self)
76
77 def MergeMessage(
78 self,
79 source,
80 destination,
81 replace_message_field=False,
82 replace_repeated_field=False,
83 ):
84 """Merges fields specified in FieldMask from source to destination.
85
86 Args:
87 source: Source message.
88 destination: The destination message to be merged into.
89 replace_message_field: Replace message field if True. Merge message field
90 if False.
91 replace_repeated_field: Replace repeated field if True. Append elements of
92 repeated field if False.
93 """
94 tree = _FieldMaskTree(self)
95 tree.MergeMessage(
96 source, destination, replace_message_field, replace_repeated_field
97 )
98
99
100def _IsValidPath(message_descriptor, path):
101 """Checks whether the path is valid for Message Descriptor."""
102 parts = path.split('.')
103 last = parts.pop()
104 for name in parts:
105 field = message_descriptor.fields_by_name.get(name)
106 if (
107 field is None
108 or field.is_repeated
109 or field.type != FieldDescriptor.TYPE_MESSAGE
110 ):
111 return False
112 message_descriptor = field.message_type
113 return last in message_descriptor.fields_by_name
114
115
116def _CheckFieldMaskMessage(message):
117 """Raises ValueError if message is not a FieldMask."""
118 message_descriptor = message.DESCRIPTOR
119 if (
120 message_descriptor.name != 'FieldMask'
121 or message_descriptor.file.name != 'google/protobuf/field_mask.proto'
122 ):
123 raise ValueError(
124 'Message {0} is not a FieldMask.'.format(message_descriptor.full_name)
125 )
126
127
128def _SnakeCaseToCamelCase(path_name):
129 """Converts a path name from snake_case to camelCase."""
130 result = []
131 after_underscore = False
132 for c in path_name:
133 if c.isupper():
134 raise ValueError(
135 'Fail to print FieldMask to Json string: Path name '
136 '{0} must not contain uppercase letters.'.format(path_name)
137 )
138 if after_underscore:
139 if c.islower():
140 result.append(c.upper())
141 after_underscore = False
142 else:
143 raise ValueError(
144 'Fail to print FieldMask to Json string: The '
145 'character after a "_" must be a lowercase letter '
146 'in path name {0}.'.format(path_name)
147 )
148 elif c == '_':
149 after_underscore = True
150 else:
151 result += c
152
153 if after_underscore:
154 raise ValueError(
155 'Fail to print FieldMask to Json string: Trailing "_" '
156 'in path name {0}.'.format(path_name)
157 )
158 return ''.join(result)
159
160
161def _CamelCaseToSnakeCase(path_name):
162 """Converts a field name from camelCase to snake_case."""
163 result = []
164 for c in path_name:
165 if c == '_':
166 raise ValueError(
167 'Fail to parse FieldMask: Path name '
168 '{0} must not contain "_"s.'.format(path_name)
169 )
170 if c.isupper():
171 result += '_'
172 result += c.lower()
173 else:
174 result += c
175 return ''.join(result)
176
177
178class _FieldMaskTree(object):
179 """Represents a FieldMask in a tree structure.
180
181 For example, given a FieldMask "foo.bar,foo.baz,bar.baz",
182 the FieldMaskTree will be:
183 [_root] -+- foo -+- bar
184 | |
185 | +- baz
186 |
187 +- bar --- baz
188 In the tree, each leaf node represents a field path.
189 """
190
191 __slots__ = ('_root',)
192
193 def __init__(self, field_mask=None):
194 """Initializes the tree by FieldMask."""
195 self._root = {}
196 if field_mask:
197 self.MergeFromFieldMask(field_mask)
198
199 def MergeFromFieldMask(self, field_mask):
200 """Merges a FieldMask to the tree."""
201 for path in field_mask.paths:
202 self.AddPath(path)
203
204 def AddPath(self, path):
205 """Adds a field path into the tree.
206
207 If the field path to add is a sub-path of an existing field path
208 in the tree (i.e., a leaf node), it means the tree already matches
209 the given path so nothing will be added to the tree. If the path
210 matches an existing non-leaf node in the tree, that non-leaf node
211 will be turned into a leaf node with all its children removed because
212 the path matches all the node's children. Otherwise, a new path will
213 be added.
214
215 Args:
216 path: The field path to add.
217 """
218 node = self._root
219 for name in path.split('.'):
220 if name not in node:
221 node[name] = {}
222 elif not node[name]:
223 # Pre-existing empty node implies we already have this entire tree.
224 return
225 node = node[name]
226 # Remove any sub-trees we might have had.
227 node.clear()
228
229 def ToFieldMask(self, field_mask):
230 """Converts the tree to a FieldMask."""
231 field_mask.Clear()
232 _AddFieldPaths(self._root, '', field_mask)
233
234 def IntersectPath(self, path, intersection):
235 """Calculates the intersection part of a field path with this tree.
236
237 Args:
238 path: The field path to calculates.
239 intersection: The out tree to record the intersection part.
240 """
241 node = self._root
242 for name in path.split('.'):
243 if name not in node:
244 return
245 elif not node[name]:
246 intersection.AddPath(path)
247 return
248 node = node[name]
249 intersection.AddLeafNodes(path, node)
250
251 def AddLeafNodes(self, prefix, node):
252 """Adds leaf nodes begin with prefix to this tree."""
253 if not node:
254 self.AddPath(prefix)
255 return
256 stack = [(prefix, node)]
257 while stack:
258 current_prefix, current_node = stack.pop()
259 if not current_node:
260 self.AddPath(current_prefix)
261 continue
262 for name in current_node:
263 child_path = current_prefix + '.' + name
264 stack.append((child_path, current_node[name]))
265
266 def MergeMessage(
267 self, source, destination, replace_message, replace_repeated
268 ):
269 """Merge all fields specified by this tree from source to destination."""
270 _MergeMessage(
271 self._root, source, destination, replace_message, replace_repeated
272 )
273
274
275def _StrConvert(value):
276 """Converts value to str if it is not."""
277 # This file is imported by c extension and some methods like ClearField
278 # requires string for the field name. py2/py3 has different text
279 # type and may use unicode.
280 if not isinstance(value, str):
281 return value.encode('utf-8')
282 return value
283
284
285def _MergeMessage(node, source, destination, replace_message, replace_repeated):
286 """Merge all fields specified by a sub-tree from source to destination."""
287 stack = [(node, source, destination)]
288 while stack:
289 current_node, current_source, current_destination = stack.pop()
290 source_descriptor = current_source.DESCRIPTOR
291 for name in current_node:
292 child = current_node[name]
293 field = source_descriptor.fields_by_name[name]
294 if field is None:
295 raise ValueError(
296 "Error: Can't find field {0} in message {1}.".format(
297 name, source_descriptor.full_name
298 )
299 )
300 if child:
301 # Sub-paths are only allowed for singular message fields.
302 if (
303 field.is_repeated
304 or field.cpp_type != FieldDescriptor.CPPTYPE_MESSAGE
305 ):
306 raise ValueError(
307 'Error: Field {0} in message {1} is not a singular '
308 'message field and cannot have sub-fields.'.format(
309 name, source_descriptor.full_name
310 )
311 )
312 if current_source.HasField(name):
313 stack.append((
314 child,
315 getattr(current_source, name),
316 getattr(current_destination, name),
317 ))
318 continue
319 if field.is_repeated:
320 if replace_repeated:
321 current_destination.ClearField(_StrConvert(name))
322 repeated_source = getattr(current_source, name)
323 repeated_destination = getattr(current_destination, name)
324 repeated_destination.MergeFrom(repeated_source)
325 else:
326 if field.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE:
327 if replace_message:
328 current_destination.ClearField(_StrConvert(name))
329 if current_source.HasField(name):
330 getattr(current_destination, name).MergeFrom(
331 getattr(current_source, name)
332 )
333 elif not field.has_presence or current_source.HasField(name):
334 setattr(current_destination, name, getattr(current_source, name))
335 else:
336 current_destination.ClearField(_StrConvert(name))
337
338
339def _AddFieldPaths(node, prefix, field_mask):
340 """Adds the field paths descended from node to field_mask."""
341 stack = [(node, prefix)]
342 while stack:
343 current_node, current_prefix = stack.pop()
344 if not current_node and current_prefix:
345 field_mask.paths.append(current_prefix)
346 continue
347 for name in sorted(current_node, reverse=True):
348 if current_prefix:
349 child_path = current_prefix + '.' + name
350 else:
351 child_path = name
352 stack.append((current_node[name], child_path))