Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/proto/message.py: 66%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

278 statements  

1# Copyright 2018 Google LLC 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# https://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14 

15import collections 

16import collections.abc 

17import copy 

18import re 

19from typing import Any, Dict, List, Optional, Type 

20import warnings 

21 

22import google.protobuf 

23from google.protobuf import descriptor_pb2 

24from google.protobuf import message 

25from google.protobuf.json_format import MessageToDict, MessageToJson, Parse 

26 

27from proto import _file_info 

28from proto import _package_info 

29from proto.fields import Field 

30from proto.fields import MapField 

31from proto.fields import RepeatedField 

32from proto.marshal import Marshal 

33from proto.primitives import ProtoType 

34from proto.utils import has_upb 

35 

36 

37PROTOBUF_VERSION = google.protobuf.__version__ 

38 

39# extract the major version code 

40_PROTOBUF_MAJOR_VERSION = PROTOBUF_VERSION.partition(".")[0] 

41 

42_upb = has_upb() # Important to cache result here. 

43 

44 

45class MessageMeta(type): 

46 """A metaclass for building and registering Message subclasses.""" 

47 

48 def __new__(mcls, name, bases, attrs): 

49 # Do not do any special behavior for Message itself. 

50 if not bases: 

51 return super().__new__(mcls, name, bases, attrs) 

52 

53 # Get the essential information about the proto package, and where 

54 # this component belongs within the file. 

55 package, marshal = _package_info.compile(name, attrs) 

56 

57 # Determine the local path of this proto component within the file. 

58 local_path = tuple(attrs.get("__qualname__", name).split(".")) 

59 

60 # Sanity check: We get the wrong full name if a class is declared 

61 # inside a function local scope; correct this. 

62 if "<locals>" in local_path: 

63 ix = local_path.index("<locals>") 

64 local_path = local_path[: ix - 1] + local_path[ix + 1 :] 

65 

66 # Determine the full name in protocol buffers. 

67 full_name = ".".join((package,) + local_path).lstrip(".") 

68 

69 # Special case: Maps. Map fields are special; they are essentially 

70 # shorthand for a nested message and a repeated field of that message. 

71 # Decompose each map into its constituent form. 

72 # https://developers.google.com/protocol-buffers/docs/proto3#maps 

73 map_fields = {} 

74 for key, field in attrs.items(): 

75 if not isinstance(field, MapField): 

76 continue 

77 

78 # Determine the name of the entry message. 

79 msg_name = "{pascal_key}Entry".format( 

80 pascal_key=re.sub( 

81 r"_\w", 

82 lambda m: m.group()[1:].upper(), 

83 key, 

84 ).replace(key[0], key[0].upper(), 1), 

85 ) 

86 

87 # Create the "entry" message (with the key and value fields). 

88 # 

89 # Note: We instantiate an ordered dictionary here and then 

90 # attach key and value in order to ensure that the fields are 

91 # iterated in the correct order when the class is created. 

92 # This is only an issue in Python 3.5, where the order is 

93 # random (and the wrong order causes the pool to refuse to add 

94 # the descriptor because reasons). 

95 entry_attrs = collections.OrderedDict( 

96 { 

97 "__module__": attrs.get("__module__", None), 

98 "__qualname__": "{prefix}.{name}".format( 

99 prefix=attrs.get("__qualname__", name), 

100 name=msg_name, 

101 ), 

102 "_pb_options": {"map_entry": True}, 

103 } 

104 ) 

105 entry_attrs["key"] = Field(field.map_key_type, number=1) 

106 entry_attrs["value"] = Field( 

107 field.proto_type, 

108 number=2, 

109 enum=field.enum, 

110 message=field.message, 

111 ) 

112 map_fields[msg_name] = MessageMeta(msg_name, (Message,), entry_attrs) 

113 

114 # Create the repeated field for the entry message. 

115 map_fields[key] = RepeatedField( 

116 ProtoType.MESSAGE, 

117 number=field.number, 

118 message=map_fields[msg_name], 

119 ) 

120 

121 # Add the new entries to the attrs 

122 attrs.update(map_fields) 

123 

124 # Okay, now we deal with all the rest of the fields. 

125 # Iterate over all the attributes and separate the fields into 

126 # their own sequence. 

127 fields = [] 

128 new_attrs = {} 

129 oneofs = collections.OrderedDict() 

130 proto_imports = set() 

131 index = 0 

132 for key, field in attrs.items(): 

133 # Sanity check: If this is not a field, do nothing. 

134 if not isinstance(field, Field): 

135 # The field objects themselves should not be direct attributes. 

136 new_attrs[key] = field 

137 continue 

138 

139 # Add data that the field requires that we do not take in the 

140 # constructor because we can derive it from the metaclass. 

141 # (The goal is to make the declaration syntax as nice as possible.) 

142 field.mcls_data = { 

143 "name": key, 

144 "parent_name": full_name, 

145 "index": index, 

146 "package": package, 

147 } 

148 

149 # Add the field to the list of fields. 

150 fields.append(field) 

151 # If this field is part of a "oneof", ensure the oneof itself 

152 # is represented. 

153 if field.oneof: 

154 # Keep a running tally of the index of each oneof, and assign 

155 # that index to the field's descriptor. 

156 oneofs.setdefault(field.oneof, len(oneofs)) 

157 field.descriptor.oneof_index = oneofs[field.oneof] 

158 

159 # If this field references a message, it may be from another 

160 # proto file; ensure we know about the import (to faithfully 

161 # construct our file descriptor proto). 

162 if field.message and not isinstance(field.message, str): 

163 field_msg = field.message 

164 if hasattr(field_msg, "pb") and callable(field_msg.pb): 

165 field_msg = field_msg.pb() 

166 # Sanity check: The field's message may not yet be defined if 

167 # it was a Message defined in the same file, and the file 

168 # descriptor proto has not yet been generated. 

169 # 

170 # We do nothing in this situation; everything will be handled 

171 # correctly when the file descriptor is created later. 

172 if field_msg: 

173 proto_imports.add(field_msg.DESCRIPTOR.file.name) 

174 

175 # Same thing, but for enums. 

176 elif field.enum and not isinstance(field.enum, str): 

177 field_enum = ( 

178 field.enum._meta.pb 

179 if hasattr(field.enum, "_meta") 

180 else field.enum.DESCRIPTOR 

181 ) 

182 

183 if field_enum: 

184 proto_imports.add(field_enum.file.name) 

185 

186 # Increment the field index counter. 

187 index += 1 

188 

189 # As per descriptor.proto, all synthetic oneofs must be ordered after 

190 # 'real' oneofs. 

191 opt_attrs = {} 

192 for field in fields: 

193 if field.optional: 

194 field.oneof = "_{}".format(field.name) 

195 field.descriptor.oneof_index = oneofs[field.oneof] = len(oneofs) 

196 opt_attrs[field.name] = field.name 

197 

198 # Generating a metaclass dynamically provides class attributes that 

199 # instances can't see. This provides idiomatically named constants 

200 # that enable the following pattern to check for field presence: 

201 # 

202 # class MyMessage(proto.Message): 

203 # field = proto.Field(proto.INT32, number=1, optional=True) 

204 # 

205 # m = MyMessage() 

206 # MyMessage.field in m 

207 if opt_attrs: 

208 mcls = type("AttrsMeta", (mcls,), opt_attrs) 

209 

210 # Determine the filename. 

211 # We determine an appropriate proto filename based on the 

212 # Python module. 

213 filename = _file_info._FileInfo.proto_file_name( 

214 new_attrs.get("__module__", name.lower()) 

215 ) 

216 

217 # Get or create the information about the file, including the 

218 # descriptor to which the new message descriptor shall be added. 

219 file_info = _file_info._FileInfo.maybe_add_descriptor(filename, package) 

220 

221 # Ensure any imports that would be necessary are assigned to the file 

222 # descriptor proto being created. 

223 for proto_import in proto_imports: 

224 if proto_import not in file_info.descriptor.dependency: 

225 file_info.descriptor.dependency.append(proto_import) 

226 

227 # Retrieve any message options. 

228 opts = descriptor_pb2.MessageOptions(**new_attrs.pop("_pb_options", {})) 

229 

230 # Create the underlying proto descriptor. 

231 desc = descriptor_pb2.DescriptorProto( 

232 name=name, 

233 field=[i.descriptor for i in fields], 

234 oneof_decl=[ 

235 descriptor_pb2.OneofDescriptorProto(name=i) for i in oneofs.keys() 

236 ], 

237 options=opts, 

238 ) 

239 

240 # If any descriptors were nested under this one, they need to be 

241 # attached as nested types here. 

242 child_paths = [p for p in file_info.nested.keys() if local_path == p[:-1]] 

243 for child_path in child_paths: 

244 desc.nested_type.add().MergeFrom(file_info.nested.pop(child_path)) 

245 

246 # Same thing, but for enums 

247 child_paths = [p for p in file_info.nested_enum.keys() if local_path == p[:-1]] 

248 for child_path in child_paths: 

249 desc.enum_type.add().MergeFrom(file_info.nested_enum.pop(child_path)) 

250 

251 # Add the descriptor to the file if it is a top-level descriptor, 

252 # or to a "holding area" for nested messages otherwise. 

253 if len(local_path) == 1: 

254 file_info.descriptor.message_type.add().MergeFrom(desc) 

255 else: 

256 file_info.nested[local_path] = desc 

257 

258 # Create the MessageInfo instance to be attached to this message. 

259 new_attrs["_meta"] = _MessageInfo( 

260 fields=fields, 

261 full_name=full_name, 

262 marshal=marshal, 

263 options=opts, 

264 package=package, 

265 ) 

266 

267 # Run the superclass constructor. 

268 cls = super().__new__(mcls, name, bases, new_attrs) 

269 

270 # The info class and fields need a reference to the class just created. 

271 cls._meta.parent = cls 

272 for field in cls._meta.fields.values(): 

273 field.parent = cls 

274 

275 # Add this message to the _FileInfo instance; this allows us to 

276 # associate the descriptor with the message once the descriptor 

277 # is generated. 

278 file_info.messages[full_name] = cls 

279 

280 # Generate the descriptor for the file if it is ready. 

281 if file_info.ready(new_class=cls): 

282 file_info.generate_file_pb(new_class=cls, fallback_salt=full_name) 

283 

284 # Done; return the class. 

285 return cls 

286 

287 @classmethod 

288 def __prepare__(mcls, name, bases, **kwargs): 

289 return collections.OrderedDict() 

290 

291 @property 

292 def meta(cls): 

293 return cls._meta 

294 

295 def __dir__(self): 

296 try: 

297 names = set(dir(type)) 

298 names.update( 

299 ( 

300 "meta", 

301 "pb", 

302 "wrap", 

303 "serialize", 

304 "deserialize", 

305 "to_json", 

306 "from_json", 

307 "to_dict", 

308 "copy_from", 

309 ) 

310 ) 

311 desc = self.pb().DESCRIPTOR 

312 names.update(t.name for t in desc.nested_types) 

313 names.update(e.name for e in desc.enum_types) 

314 

315 return names 

316 except AttributeError: 

317 return dir(type) 

318 

319 def pb(cls, obj=None, *, coerce: bool = False): 

320 """Return the underlying protobuf Message class or instance. 

321 

322 Args: 

323 obj: If provided, and an instance of ``cls``, return the 

324 underlying protobuf instance. 

325 coerce (bool): If provided, will attempt to coerce ``obj`` to 

326 ``cls`` if it is not already an instance. 

327 """ 

328 if obj is None: 

329 return cls.meta.pb 

330 if not isinstance(obj, cls): 

331 if coerce: 

332 obj = cls(obj) 

333 else: 

334 raise TypeError( 

335 "%r is not an instance of %s" 

336 % ( 

337 obj, 

338 cls.__name__, 

339 ) 

340 ) 

341 return obj._pb 

342 

343 def wrap(cls, pb): 

344 """Return a Message object that shallowly wraps the descriptor. 

345 

346 Args: 

347 pb: A protocol buffer object, such as would be returned by 

348 :meth:`pb`. 

349 """ 

350 # Optimized fast path. 

351 instance = cls.__new__(cls) 

352 super(cls, instance).__setattr__("_pb", pb) 

353 return instance 

354 

355 def serialize(cls, instance) -> bytes: 

356 """Return the serialized proto. 

357 

358 Args: 

359 instance: An instance of this message type, or something 

360 compatible (accepted by the type's constructor). 

361 

362 Returns: 

363 bytes: The serialized representation of the protocol buffer. 

364 """ 

365 return cls.pb(instance, coerce=True).SerializeToString() 

366 

367 def deserialize(cls, payload: bytes) -> "Message": 

368 """Given a serialized proto, deserialize it into a Message instance. 

369 

370 Args: 

371 payload (bytes): The serialized proto. 

372 

373 Returns: 

374 ~.Message: An instance of the message class against which this 

375 method was called. 

376 """ 

377 return cls.wrap(cls.pb().FromString(payload)) 

378 

379 def _warn_if_including_default_value_fields_is_used_protobuf_5( 

380 cls, including_default_value_fields: Optional[bool] 

381 ) -> None: 

382 """ 

383 Warn Protobuf 5.x+ users that `including_default_value_fields` is deprecated if it is set. 

384 

385 Args: 

386 including_default_value_fields (Optional(bool)): The value of `including_default_value_fields` set by the user. 

387 """ 

388 if ( 

389 _PROTOBUF_MAJOR_VERSION not in ("3", "4") 

390 and including_default_value_fields is not None 

391 ): 

392 warnings.warn( 

393 """The argument `including_default_value_fields` has been removed from 

394 Protobuf 5.x. Please use `always_print_fields_with_no_presence` instead. 

395 """, 

396 DeprecationWarning, 

397 ) 

398 

399 def _raise_if_print_fields_values_are_set_and_differ( 

400 cls, 

401 always_print_fields_with_no_presence: Optional[bool], 

402 including_default_value_fields: Optional[bool], 

403 ) -> None: 

404 """ 

405 Raise Exception if both `always_print_fields_with_no_presence` and `including_default_value_fields` are set 

406 and the values differ. 

407 

408 Args: 

409 always_print_fields_with_no_presence (Optional(bool)): The value of `always_print_fields_with_no_presence` set by the user. 

410 including_default_value_fields (Optional(bool)): The value of `including_default_value_fields` set by the user. 

411 Returns: 

412 None 

413 Raises: 

414 ValueError: if both `always_print_fields_with_no_presence` and `including_default_value_fields` are set and 

415 the values differ. 

416 """ 

417 if ( 

418 always_print_fields_with_no_presence is not None 

419 and including_default_value_fields is not None 

420 and always_print_fields_with_no_presence != including_default_value_fields 

421 ): 

422 raise ValueError( 

423 "Arguments `always_print_fields_with_no_presence` and `including_default_value_fields` must match" 

424 ) 

425 

426 def _normalize_print_fields_without_presence( 

427 cls, 

428 always_print_fields_with_no_presence: Optional[bool], 

429 including_default_value_fields: Optional[bool], 

430 ) -> bool: 

431 """ 

432 Return true if fields with no presence should be included in the results. 

433 By default, fields with no presence will be included in the results 

434 when both `always_print_fields_with_no_presence` and 

435 `including_default_value_fields` are not set 

436 

437 Args: 

438 always_print_fields_with_no_presence (Optional(bool)): The value of `always_print_fields_with_no_presence` set by the user. 

439 including_default_value_fields (Optional(bool)): The value of `including_default_value_fields` set by the user. 

440 Returns: 

441 None 

442 Raises: 

443 ValueError: if both `always_print_fields_with_no_presence` and `including_default_value_fields` are set and 

444 the values differ. 

445 """ 

446 

447 cls._warn_if_including_default_value_fields_is_used_protobuf_5( 

448 including_default_value_fields 

449 ) 

450 cls._raise_if_print_fields_values_are_set_and_differ( 

451 always_print_fields_with_no_presence, including_default_value_fields 

452 ) 

453 # Default to True if neither `always_print_fields_with_no_presence` or `including_default_value_fields` is set 

454 return ( 

455 ( 

456 always_print_fields_with_no_presence is None 

457 and including_default_value_fields is None 

458 ) 

459 or always_print_fields_with_no_presence 

460 or including_default_value_fields 

461 ) 

462 

463 def to_json( 

464 cls, 

465 instance, 

466 *, 

467 use_integers_for_enums=True, 

468 including_default_value_fields=None, 

469 preserving_proto_field_name=False, 

470 sort_keys=False, 

471 indent=2, 

472 float_precision=None, 

473 always_print_fields_with_no_presence=None, 

474 ) -> str: 

475 """Given a message instance, serialize it to json 

476 

477 Args: 

478 instance: An instance of this message type, or something 

479 compatible (accepted by the type's constructor). 

480 use_integers_for_enums (Optional(bool)): An option that determines whether enum 

481 values should be represented by strings (False) or integers (True). 

482 Default is True. 

483 including_default_value_fields (Optional(bool)): Deprecated. Use argument 

484 `always_print_fields_with_no_presence` instead. An option that 

485 determines whether the default field values should be included in the results. 

486 This value must match `always_print_fields_with_no_presence`, 

487 if both arguments are explicitly set. 

488 preserving_proto_field_name (Optional(bool)): An option that 

489 determines whether field name representations preserve 

490 proto case (snake_case) or use lowerCamelCase. Default is False. 

491 sort_keys (Optional(bool)): If True, then the output will be sorted by field names. 

492 Default is False. 

493 indent (Optional(int)): The JSON object will be pretty-printed with this indent level. 

494 An indent level of 0 or negative will only insert newlines. 

495 Pass None for the most compact representation without newlines. 

496 float_precision (Optional(int)): If set, use this to specify float field valid digits. 

497 Default is None. [DEPRECATED] float_precision was removed in Protobuf 7.x. 

498 always_print_fields_with_no_presence (Optional(bool)): If True, fields without 

499 presence (implicit presence scalars, repeated fields, and map fields) will 

500 always be serialized. Any field that supports presence is not affected by 

501 this option (including singular message fields and oneof fields). 

502 This value must match `including_default_value_fields`, 

503 if both arguments are explicitly set. 

504 Returns: 

505 str: The json string representation of the protocol buffer. 

506 """ 

507 return _message_to_map(map_fn=MessageToJson, **locals()) 

508 

509 def from_json(cls, payload, *, ignore_unknown_fields=False) -> "Message": 

510 """Given a json string representing an instance, 

511 parse it into a message. 

512 

513 Args: 

514 payload: A json string representing a message. 

515 ignore_unknown_fields (Optional(bool)): If True, do not raise errors 

516 for unknown fields. 

517 

518 Returns: 

519 ~.Message: An instance of the message class against which this 

520 method was called. 

521 """ 

522 instance = cls() 

523 Parse(payload, instance._pb, ignore_unknown_fields=ignore_unknown_fields) 

524 return instance 

525 

526 def to_dict( 

527 cls, 

528 instance, 

529 *, 

530 use_integers_for_enums=True, 

531 preserving_proto_field_name=True, 

532 including_default_value_fields=None, 

533 float_precision=None, 

534 always_print_fields_with_no_presence=None, 

535 ) -> Dict[str, Any]: 

536 """Given a message instance, return its representation as a python dict. 

537 

538 Args: 

539 instance: An instance of this message type, or something 

540 compatible (accepted by the type's constructor). 

541 use_integers_for_enums (Optional(bool)): An option that determines whether enum 

542 values should be represented by strings (False) or integers (True). 

543 Default is True. 

544 preserving_proto_field_name (Optional(bool)): An option that 

545 determines whether field name representations preserve 

546 proto case (snake_case) or use lowerCamelCase. Default is True. 

547 including_default_value_fields (Optional(bool)): Deprecated. Use argument 

548 `always_print_fields_with_no_presence` instead. An option that 

549 determines whether the default field values should be included in the results. 

550 This value must match `always_print_fields_with_no_presence`, 

551 if both arguments are explicitly set. 

552 float_precision (Optional(int)): If set, use this to specify float field valid digits. 

553 Default is None. [DEPRECATED] float_precision was removed in Protobuf 7.x. 

554 always_print_fields_with_no_presence (Optional(bool)): If True, fields without 

555 presence (implicit presence scalars, repeated fields, and map fields) will 

556 always be serialized. Any field that supports presence is not affected by 

557 this option (including singular message fields and oneof fields). This value 

558 must match `including_default_value_fields`, if both arguments are explicitly set. 

559 

560 Returns: 

561 dict: A representation of the protocol buffer using pythonic data structures. 

562 Messages and map fields are represented as dicts, 

563 repeated fields are represented as lists. 

564 """ 

565 return _message_to_map(map_fn=MessageToDict, **locals()) 

566 

567 def copy_from(cls, instance, other): 

568 """Equivalent for protobuf.Message.CopyFrom 

569 

570 Args: 

571 instance: An instance of this message type 

572 other: (Union[dict, ~.Message): 

573 A dictionary or message to reinitialize the values for this message. 

574 """ 

575 if isinstance(other, cls): 

576 # Just want the underlying proto. 

577 other = Message.pb(other) 

578 elif isinstance(other, cls.pb()): 

579 # Don't need to do anything. 

580 pass 

581 elif isinstance(other, collections.abc.Mapping): 

582 # Coerce into a proto 

583 other = cls._meta.pb(**other) 

584 else: 

585 raise TypeError( 

586 "invalid argument type to copy to {}: {}".format( 

587 cls.__name__, other.__class__.__name__ 

588 ) 

589 ) 

590 

591 # Note: we can't just run self.__init__ because this may be a message field 

592 # for a higher order proto; the memory layout for protos is NOT LIKE the 

593 # python memory model. We cannot rely on just setting things by reference. 

594 # Non-trivial complexity is (partially) hidden by the protobuf runtime. 

595 cls.pb(instance).CopyFrom(other) 

596 

597 

598class Message(metaclass=MessageMeta): 

599 """The abstract base class for a message. 

600 

601 Args: 

602 mapping (Union[dict, ~.Message]): A dictionary or message to be 

603 used to determine the values for this message. 

604 ignore_unknown_fields (Optional(bool)): If True, do not raise errors for 

605 unknown fields. Only applied if `mapping` is a mapping type or there 

606 are keyword parameters. 

607 kwargs (dict): Keys and values corresponding to the fields of the 

608 message. 

609 """ 

610 

611 def __init__( 

612 self, 

613 mapping=None, 

614 *, 

615 ignore_unknown_fields=False, 

616 **kwargs, 

617 ): 

618 # We accept several things for `mapping`: 

619 # * An instance of this class. 

620 # * An instance of the underlying protobuf descriptor class. 

621 # * A dict 

622 # * Nothing (keyword arguments only). 

623 if mapping is None: 

624 if not kwargs: 

625 # Special fast path for empty construction. 

626 super().__setattr__("_pb", self._meta.pb()) 

627 return 

628 

629 mapping = kwargs 

630 elif isinstance(mapping, self._meta.pb): 

631 # Make a copy of the mapping. 

632 # This is a constructor for a new object, so users will assume 

633 # that it will not have side effects on the arguments being 

634 # passed in. 

635 # 

636 # The `wrap` method on the metaclass is the public API for taking 

637 # ownership of the passed in protobuf object. 

638 mapping = copy.deepcopy(mapping) 

639 if kwargs: 

640 mapping.MergeFrom(self._meta.pb(**kwargs)) 

641 

642 super().__setattr__("_pb", mapping) 

643 return 

644 elif isinstance(mapping, type(self)): 

645 # Just use the above logic on mapping's underlying pb. 

646 self.__init__(mapping=mapping._pb, **kwargs) 

647 return 

648 elif isinstance(mapping, collections.abc.Mapping): 

649 # Can't have side effects on mapping. 

650 mapping = copy.copy(mapping) 

651 # kwargs entries take priority for duplicate keys. 

652 mapping.update(kwargs) 

653 else: 

654 # Sanity check: Did we get something not a map? Error if so. 

655 raise TypeError( 

656 "Invalid constructor input for %s: %r" 

657 % ( 

658 self.__class__.__name__, 

659 mapping, 

660 ) 

661 ) 

662 

663 params = {} 

664 # Update the mapping to address any values that need to be 

665 # coerced. 

666 marshal = self._meta.marshal 

667 for key, value in mapping.items(): 

668 (key, pb_type) = self._get_pb_type_from_key(key) 

669 if pb_type is None: 

670 if ignore_unknown_fields: 

671 continue 

672 

673 raise ValueError( 

674 "Unknown field for {}: {}".format(self.__class__.__name__, key) 

675 ) 

676 

677 pb_value = marshal.to_proto(pb_type, value) 

678 

679 if pb_value is not None: 

680 params[key] = pb_value 

681 

682 # Create the internal protocol buffer. 

683 super().__setattr__("_pb", self._meta.pb(**params)) 

684 

685 def _get_pb_type_from_key(self, key): 

686 """Given a key, return the corresponding pb_type. 

687 

688 Args: 

689 key(str): The name of the field. 

690 

691 Returns: 

692 A tuple containing a key and pb_type. The pb_type will be 

693 the composite type of the field, or the primitive type if a primitive. 

694 If no corresponding field exists, return None. 

695 """ 

696 

697 pb_type = None 

698 

699 try: 

700 pb_type = self._meta.fields[key].pb_type 

701 except KeyError: 

702 # Underscores may be appended to field names 

703 # that collide with python or proto-plus keywords. 

704 # In case a key only exists with a `_` suffix, coerce the key 

705 # to include the `_` suffix. It's not possible to 

706 # natively define the same field with a trailing underscore in protobuf. 

707 # See related issue 

708 # https://github.com/googleapis/python-api-core/issues/227 

709 if f"{key}_" in self._meta.fields: 

710 key = f"{key}_" 

711 pb_type = self._meta.fields[key].pb_type 

712 

713 return (key, pb_type) 

714 

715 def __dir__(self): 

716 desc = type(self).pb().DESCRIPTOR 

717 names = {f_name for f_name in self._meta.fields.keys()} 

718 names.update(m.name for m in desc.nested_types) 

719 names.update(e.name for e in desc.enum_types) 

720 names.update(dir(object())) 

721 # Can't think of a better way of determining 

722 # the special methods than manually listing them. 

723 names.update( 

724 ( 

725 "__bool__", 

726 "__contains__", 

727 "__dict__", 

728 "__getattr__", 

729 "__getstate__", 

730 "__module__", 

731 "__setstate__", 

732 "__weakref__", 

733 ) 

734 ) 

735 

736 return names 

737 

738 def __bool__(self): 

739 """Return True if any field is truthy, False otherwise.""" 

740 return any(k in self and getattr(self, k) for k in self._meta.fields.keys()) 

741 

742 def __contains__(self, key): 

743 """Return True if this field was set to something non-zero on the wire. 

744 

745 In most cases, this method will return True when ``__getattr__`` 

746 would return a truthy value and False when it would return a falsy 

747 value, so explicitly calling this is not useful. 

748 

749 The exception case is empty messages explicitly set on the wire, 

750 which are falsy from ``__getattr__``. This method allows to 

751 distinguish between an explicitly provided empty message and the 

752 absence of that message, which is useful in some edge cases. 

753 

754 The most common edge case is the use of ``google.protobuf.BoolValue`` 

755 to get a boolean that distinguishes between ``False`` and ``None`` 

756 (or the same for a string, int, etc.). This library transparently 

757 handles that case for you, but this method remains available to 

758 accommodate cases not automatically covered. 

759 

760 Args: 

761 key (str): The name of the field. 

762 

763 Returns: 

764 bool: Whether the field's value corresponds to a non-empty 

765 wire serialization. 

766 """ 

767 pb_value = getattr(self._pb, key) 

768 try: 

769 # Protocol buffers "HasField" is unfriendly; it only works 

770 # against composite, non-repeated fields, and raises ValueError 

771 # against any repeated field or primitive. 

772 # 

773 # There is no good way to test whether it is valid to provide 

774 # a field to this method, so sadly we are stuck with a 

775 # somewhat inefficient try/except. 

776 return self._pb.HasField(key) 

777 except ValueError: 

778 return bool(pb_value) 

779 

780 def __delattr__(self, key): 

781 """Delete the value on the given field. 

782 

783 This is generally equivalent to setting a falsy value. 

784 """ 

785 self._pb.ClearField(key) 

786 

787 def __eq__(self, other): 

788 """Return True if the messages are equal, False otherwise.""" 

789 # If these are the same type, use internal protobuf's equality check. 

790 if isinstance(other, type(self)): 

791 return self._pb == other._pb 

792 

793 # If the other type is the target protobuf object, honor that also. 

794 if isinstance(other, self._meta.pb): 

795 return self._pb == other 

796 

797 # Ask the other object. 

798 return NotImplemented 

799 

800 def __getattr__(self, key): 

801 """Retrieve the given field's value. 

802 

803 In protocol buffers, the presence of a field on a message is 

804 sufficient for it to always be "present". 

805 

806 For primitives, a value of the correct type will always be returned 

807 (the "falsy" values in protocol buffers consistently match those 

808 in Python). For repeated fields, the falsy value is always an empty 

809 sequence. 

810 

811 For messages, protocol buffers does distinguish between an empty 

812 message and absence, but this distinction is subtle and rarely 

813 relevant. Therefore, this method always returns an empty message 

814 (following the official implementation). To check for message 

815 presence, use ``key in self`` (in other words, ``__contains__``). 

816 

817 .. note:: 

818 

819 Some well-known protocol buffer types 

820 (e.g. ``google.protobuf.Timestamp``) will be converted to 

821 their Python equivalents. See the ``marshal`` module for 

822 more details. 

823 """ 

824 (key, pb_type) = self._get_pb_type_from_key(key) 

825 if pb_type is None: 

826 raise AttributeError( 

827 "Unknown field for {}: {}".format(self.__class__.__name__, key) 

828 ) 

829 pb_value = getattr(self._pb, key) 

830 marshal = self._meta.marshal 

831 return marshal.to_python(pb_type, pb_value, absent=key not in self) 

832 

833 def __ne__(self, other): 

834 """Return True if the messages are unequal, False otherwise.""" 

835 return not self == other 

836 

837 def __repr__(self): 

838 return repr(self._pb) 

839 

840 def __setattr__(self, key, value): 

841 """Set the value on the given field. 

842 

843 For well-known protocol buffer types which are marshalled, either 

844 the protocol buffer object or the Python equivalent is accepted. 

845 """ 

846 if key[0] == "_": 

847 return super().__setattr__(key, value) 

848 marshal = self._meta.marshal 

849 (key, pb_type) = self._get_pb_type_from_key(key) 

850 if pb_type is None: 

851 raise AttributeError( 

852 "Unknown field for {}: {}".format(self.__class__.__name__, key) 

853 ) 

854 

855 pb_value = marshal.to_proto(pb_type, value) 

856 

857 # Clear the existing field. 

858 # This is the only way to successfully write nested falsy values, 

859 # because otherwise MergeFrom will no-op on them. 

860 self._pb.ClearField(key) 

861 

862 # Merge in the value being set. 

863 if pb_value is not None: 

864 self._pb.MergeFrom(self._meta.pb(**{key: pb_value})) 

865 

866 def __getstate__(self): 

867 """Serialize for pickling.""" 

868 return self._pb.SerializeToString() 

869 

870 def __setstate__(self, value): 

871 """Deserialization for pickling.""" 

872 new_pb = self._meta.pb().FromString(value) 

873 super().__setattr__("_pb", new_pb) 

874 

875 

876class _MessageInfo: 

877 """Metadata about a message. 

878 

879 Args: 

880 fields (Tuple[~.fields.Field]): The fields declared on the message. 

881 package (str): The proto package. 

882 full_name (str): The full name of the message. 

883 file_info (~._FileInfo): The file descriptor and messages for the 

884 file containing this message. 

885 marshal (~.Marshal): The marshal instance to which this message was 

886 automatically registered. 

887 options (~.descriptor_pb2.MessageOptions): Any options that were 

888 set on the message. 

889 """ 

890 

891 def __init__( 

892 self, 

893 *, 

894 fields: List[Field], 

895 package: str, 

896 full_name: str, 

897 marshal: Marshal, 

898 options: descriptor_pb2.MessageOptions, 

899 ) -> None: 

900 self.package = package 

901 self.full_name = full_name 

902 self.options = options 

903 self.fields = collections.OrderedDict((i.name, i) for i in fields) 

904 self.fields_by_number = collections.OrderedDict((i.number, i) for i in fields) 

905 self.marshal = marshal 

906 self._pb = None 

907 

908 @property 

909 def pb(self) -> Type[message.Message]: 

910 """Return the protobuf message type for this descriptor. 

911 

912 If a field on the message references another message which has not 

913 loaded, then this method returns None. 

914 """ 

915 return self._pb 

916 

917 

918def _message_to_map( 

919 cls, 

920 map_fn, 

921 instance, 

922 *, 

923 including_default_value_fields=None, 

924 always_print_fields_with_no_presence=None, 

925 float_precision=None, 

926 **kwargs, 

927): 

928 """ 

929 Helper for logic for Message.to_dict and Message.to_json 

930 """ 

931 

932 # The `including_default_value_fields` argument was removed from protobuf 5.x 

933 # and replaced with `always_print_fields_with_no_presence` which is similar but 

934 # handles optional fields consistently by not affecting them. 

935 # The old flag accidentally had inconsistent behavior between optional fields 

936 # in proto2 and proto3. 

937 print_fields = cls._normalize_print_fields_without_presence( 

938 always_print_fields_with_no_presence, including_default_value_fields 

939 ) 

940 if _PROTOBUF_MAJOR_VERSION in ("3", "4"): 

941 kwargs["including_default_value_fields"] = print_fields 

942 else: 

943 kwargs["always_print_fields_with_no_presence"] = print_fields 

944 

945 if float_precision: 

946 # float_precision removed in protobuf 7 

947 if _PROTOBUF_MAJOR_VERSION in ("3", "4", "5", "6"): 

948 kwargs["float_precision"] = float_precision 

949 warning_msg = "`float_precision` will be removed in Protobuf 7.x." 

950 

951 else: # pragma: NO COVER 

952 warning_msg = ( 

953 "`float_precision` was removed in Protobuf 7.x+, and will be ignored." 

954 ) 

955 warnings.warn(warning_msg, DeprecationWarning, stacklevel=3) 

956 # suppress similar float_precision warning from protobuf library. 

957 with warnings.catch_warnings(): 

958 warnings.filterwarnings("ignore", message=".*float_precision.*") 

959 return map_fn(cls.pb(instance), **kwargs) 

960 

961 

962__all__ = ("Message",)