Skip to content

Commit 9a2a1ee

Browse files
authored
fix: additional logic to mitigate collisions with reserved terms (#301)
* fix: additional logic to mitigate collisions with reserved terms * address review feedback * remove line break * remove redundant code * add assert
1 parent 989902c commit 9a2a1ee

2 files changed

Lines changed: 114 additions & 25 deletions

File tree

packages/proto-plus/proto/message.py

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -527,34 +527,72 @@ def __init__(
527527
# coerced.
528528
marshal = self._meta.marshal
529529
for key, value in mapping.items():
530+
(key, pb_type) = self._get_pb_type_from_key(key)
531+
if pb_type is None:
532+
if ignore_unknown_fields:
533+
continue
534+
535+
raise ValueError(
536+
"Unknown field for {}: {}".format(self.__class__.__name__, key)
537+
)
538+
530539
try:
531-
pb_type = self._meta.fields[key].pb_type
532-
except KeyError:
540+
pb_value = marshal.to_proto(pb_type, value)
541+
except ValueError:
533542
# Underscores may be appended to field names
534543
# that collide with python or proto-plus keywords.
535544
# In case a key only exists with a `_` suffix, coerce the key
536-
# to include the `_` suffix. Is not possible to
545+
# to include the `_` suffix. It's not possible to
537546
# natively define the same field with a trailing underscore in protobuf.
538547
# See related issue
539548
# https://github.com/googleapis/python-api-core/issues/227
540-
if f"{key}_" in self._meta.fields:
541-
key = f"{key}_"
542-
pb_type = self._meta.fields[key].pb_type
543-
else:
544-
if ignore_unknown_fields:
545-
continue
546-
547-
raise ValueError(
548-
"Unknown field for {}: {}".format(self.__class__.__name__, key)
549-
)
550-
551-
pb_value = marshal.to_proto(pb_type, value)
549+
if isinstance(value, dict):
550+
keys_to_update = [
551+
item
552+
for item in value
553+
if not hasattr(pb_type, item) and hasattr(pb_type, f"{item}_")
554+
]
555+
for item in keys_to_update:
556+
value[f"{item}_"] = value.pop(item)
557+
558+
pb_value = marshal.to_proto(pb_type, value)
559+
552560
if pb_value is not None:
553561
params[key] = pb_value
554562

555563
# Create the internal protocol buffer.
556564
super().__setattr__("_pb", self._meta.pb(**params))
557565

566+
def _get_pb_type_from_key(self, key):
567+
"""Given a key, return the corresponding pb_type.
568+
569+
Args:
570+
key(str): The name of the field.
571+
572+
Returns:
573+
A tuple containing a key and pb_type. The pb_type will be
574+
the composite type of the field, or the primitive type if a primitive.
575+
If no corresponding field exists, return None.
576+
"""
577+
578+
pb_type = None
579+
580+
try:
581+
pb_type = self._meta.fields[key].pb_type
582+
except KeyError:
583+
# Underscores may be appended to field names
584+
# that collide with python or proto-plus keywords.
585+
# In case a key only exists with a `_` suffix, coerce the key
586+
# to include the `_` suffix. It's not possible to
587+
# natively define the same field with a trailing underscore in protobuf.
588+
# See related issue
589+
# https://github.com/googleapis/python-api-core/issues/227
590+
if f"{key}_" in self._meta.fields:
591+
key = f"{key}_"
592+
pb_type = self._meta.fields[key].pb_type
593+
594+
return (key, pb_type)
595+
558596
def __dir__(self):
559597
desc = type(self).pb().DESCRIPTOR
560598
names = {f_name for f_name in self._meta.fields.keys()}
@@ -664,13 +702,14 @@ def __getattr__(self, key):
664702
their Python equivalents. See the ``marshal`` module for
665703
more details.
666704
"""
667-
try:
668-
pb_type = self._meta.fields[key].pb_type
669-
pb_value = getattr(self._pb, key)
670-
marshal = self._meta.marshal
671-
return marshal.to_python(pb_type, pb_value, absent=key not in self)
672-
except KeyError as ex:
673-
raise AttributeError(str(ex))
705+
(key, pb_type) = self._get_pb_type_from_key(key)
706+
if pb_type is None:
707+
raise AttributeError(
708+
"Unknown field for {}: {}".format(self.__class__.__name__, key)
709+
)
710+
pb_value = getattr(self._pb, key)
711+
marshal = self._meta.marshal
712+
return marshal.to_python(pb_type, pb_value, absent=key not in self)
674713

675714
def __ne__(self, other):
676715
"""Return True if the messages are unequal, False otherwise."""
@@ -688,7 +727,12 @@ def __setattr__(self, key, value):
688727
if key[0] == "_":
689728
return super().__setattr__(key, value)
690729
marshal = self._meta.marshal
691-
pb_type = self._meta.fields[key].pb_type
730+
(key, pb_type) = self._get_pb_type_from_key(key)
731+
if pb_type is None:
732+
raise AttributeError(
733+
"Unknown field for {}: {}".format(self.__class__.__name__, key)
734+
)
735+
692736
pb_value = marshal.to_proto(pb_type, value)
693737

694738
# Clear the existing field.

packages/proto-plus/tests/test_fields_mitigate_collision.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313
# limitations under the License.
1414

1515
import proto
16-
16+
import pytest
1717

1818
# Underscores may be appended to field names
1919
# that collide with python or proto-plus keywords.
2020
# In case a key only exists with a `_` suffix, coerce the key
21-
# to include the `_` suffix. Is not possible to
21+
# to include the `_` suffix. It's not possible to
2222
# natively define the same field with a trailing underscore in protobuf.
2323
# See related issue
2424
# https://github.com/googleapis/python-api-core/issues/227
@@ -27,10 +27,55 @@ class TestMessage(proto.Message):
2727
spam_ = proto.Field(proto.STRING, number=1)
2828
eggs = proto.Field(proto.STRING, number=2)
2929

30+
class TextStream(proto.Message):
31+
text_stream = proto.Field(TestMessage, number=1)
32+
3033
obj = TestMessage(spam_="has_spam")
3134
obj.eggs = "has_eggs"
3235
assert obj.spam_ == "has_spam"
3336

3437
# Test that `spam` is coerced to `spam_`
3538
modified_obj = TestMessage({"spam": "has_spam", "eggs": "has_eggs"})
3639
assert modified_obj.spam_ == "has_spam"
40+
41+
# Test get and set
42+
modified_obj.spam = "no_spam"
43+
assert modified_obj.spam == "no_spam"
44+
45+
modified_obj.spam_ = "yes_spam"
46+
assert modified_obj.spam_ == "yes_spam"
47+
48+
modified_obj.spam = "maybe_spam"
49+
assert modified_obj.spam_ == "maybe_spam"
50+
51+
modified_obj.spam_ = "maybe_not_spam"
52+
assert modified_obj.spam == "maybe_not_spam"
53+
54+
# Try nested values
55+
modified_obj = TextStream(
56+
text_stream=TestMessage({"spam": "has_spam", "eggs": "has_eggs"})
57+
)
58+
assert modified_obj.text_stream.spam_ == "has_spam"
59+
60+
# Test get and set for nested values
61+
modified_obj.text_stream.spam = "no_spam"
62+
assert modified_obj.text_stream.spam == "no_spam"
63+
64+
modified_obj.text_stream.spam_ = "yes_spam"
65+
assert modified_obj.text_stream.spam_ == "yes_spam"
66+
67+
modified_obj.text_stream.spam = "maybe_spam"
68+
assert modified_obj.text_stream.spam_ == "maybe_spam"
69+
70+
modified_obj.text_stream.spam_ = "maybe_not_spam"
71+
assert modified_obj.text_stream.spam == "maybe_not_spam"
72+
73+
with pytest.raises(AttributeError):
74+
assert modified_obj.text_stream.attribute_does_not_exist == "n/a"
75+
76+
with pytest.raises(AttributeError):
77+
modified_obj.text_stream.attribute_does_not_exist = "n/a"
78+
79+
# Try using dict
80+
modified_obj = TextStream(text_stream={"spam": "has_spam", "eggs": "has_eggs"})
81+
assert modified_obj.text_stream.spam_ == "has_spam"

0 commit comments

Comments
 (0)