From 5a88a0e05e3f228e0c8ce8ab6d27144aa78b5516 Mon Sep 17 00:00:00 2001 From: Arima Gupta Date: Thu, 9 Jul 2026 13:33:47 +0530 Subject: [PATCH 1/4] fix(proto-plus): add context to TypeErrors during message manipulation --- packages/proto-plus/proto/message.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/proto-plus/proto/message.py b/packages/proto-plus/proto/message.py index 123e58a1c5fe..1682bf8d54af 100644 --- a/packages/proto-plus/proto/message.py +++ b/packages/proto-plus/proto/message.py @@ -680,7 +680,13 @@ def __init__( params[key] = pb_value # Create the internal protocol buffer. - super().__setattr__("_pb", self._meta.pb(**params)) + try: + super().__setattr__("_pb", self._meta.pb(**params)) + except TypeError as ex: + raise TypeError( + f"Failed to initialize {self.__class__.__name__} due to invalid type " + f"or structure in parameters. Underlying error: {ex}" + ) from ex def _get_pb_type_from_key(self, key): """Given a key, return the corresponding pb_type. @@ -861,7 +867,14 @@ def __setattr__(self, key, value): # Merge in the value being set. if pb_value is not None: - self._pb.MergeFrom(self._meta.pb(**{key: pb_value})) + try: + self._pb.MergeFrom(self._meta.pb(**{key: pb_value})) + except TypeError as ex: + raise TypeError( + f"Failed to set field '{key}' on {self.__class__.__name__} with value {value!r}. " + f"Underlying error: {ex}" + ) from ex + def __getstate__(self): """Serialize for pickling.""" From 8396d8b71bd7df144f0e5be94609fe3ae309ec63 Mon Sep 17 00:00:00 2001 From: Arima Gupta Date: Thu, 16 Jul 2026 19:07:56 +0530 Subject: [PATCH 2/4] fix(proto-plus): include the invalid value in __setattr__ TypeError messages --- packages/proto-plus/proto/message.py | 67 +++++++++++++++------------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/packages/proto-plus/proto/message.py b/packages/proto-plus/proto/message.py index 1682bf8d54af..29bca5218806 100644 --- a/packages/proto-plus/proto/message.py +++ b/packages/proto-plus/proto/message.py @@ -659,34 +659,35 @@ def __init__( mapping, ) ) - params = {} # Update the mapping to address any values that need to be # coerced. marshal = self._meta.marshal - for key, value in mapping.items(): - (key, pb_type) = self._get_pb_type_from_key(key) - if pb_type is None: - if ignore_unknown_fields: - continue - - raise ValueError( - "Unknown field for {}: {}".format(self.__class__.__name__, key) - ) + try: + for key, value in mapping.items(): + (key, pb_type) = self._get_pb_type_from_key(key) + if pb_type is None: + if ignore_unknown_fields: + continue + + raise ValueError( + "Unknown field for {}: {}".format(self.__class__.__name__, key) + ) - pb_value = marshal.to_proto(pb_type, value) + pb_value = marshal.to_proto(pb_type, value) - if pb_value is not None: - params[key] = pb_value + if pb_value is not None: + params[key] = pb_value - # Create the internal protocol buffer. - try: + # Create the internal protocol buffer. super().__setattr__("_pb", self._meta.pb(**params)) - except TypeError as ex: + except TypeError as e: raise TypeError( - f"Failed to initialize {self.__class__.__name__} due to invalid type " - f"or structure in parameters. Underlying error: {ex}" - ) from ex + "Failed to initialize {}. Underlying error: {}".format( + self.__class__.__name__, e + ) + ) from e + def _get_pb_type_from_key(self, key): """Given a key, return the corresponding pb_type. @@ -858,22 +859,24 @@ def __setattr__(self, key, value): "Unknown field for {}: {}".format(self.__class__.__name__, key) ) - pb_value = marshal.to_proto(pb_type, value) + try: + pb_value = marshal.to_proto(pb_type, value) - # Clear the existing field. - # This is the only way to successfully write nested falsy values, - # because otherwise MergeFrom will no-op on them. - self._pb.ClearField(key) + # Clear the existing field. + # This is the only way to successfully write nested falsy values, + # because otherwise MergeFrom will no-op on them. + self._pb.ClearField(key) - # Merge in the value being set. - if pb_value is not None: - try: + # Merge in the value being set. + if pb_value is not None: self._pb.MergeFrom(self._meta.pb(**{key: pb_value})) - except TypeError as ex: - raise TypeError( - f"Failed to set field '{key}' on {self.__class__.__name__} with value {value!r}. " - f"Underlying error: {ex}" - ) from ex + except TypeError as e: + raise TypeError( + "Failed to set field '{}' on {} to value {}. Underlying error: {}".format( + key, self.__class__.__name__, value, e + ) + ) from e + def __getstate__(self): From 4b24553b86951dbcc1963260f58a2dc18d072ed1 Mon Sep 17 00:00:00 2001 From: Arima Gupta Date: Thu, 16 Jul 2026 19:12:29 +0530 Subject: [PATCH 3/4] test(proto-plus): add regression tests and guard descriptor check in marshal --- packages/proto-plus/proto/marshal/marshal.py | 3 +- packages/proto-plus/tests/test_message.py | 33 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/proto-plus/proto/marshal/marshal.py b/packages/proto-plus/proto/marshal/marshal.py index 81f1a473bf8f..3133467560d8 100644 --- a/packages/proto-plus/proto/marshal/marshal.py +++ b/packages/proto-plus/proto/marshal/marshal.py @@ -224,7 +224,8 @@ def to_proto(self, proto_type, value, *, strict: bool = False): # annotation. We need to do the conversion based on the `value` # field's type. if isinstance(value, dict) and ( - proto_type.DESCRIPTOR.has_options + hasattr(proto_type, "DESCRIPTOR") + and proto_type.DESCRIPTOR.has_options and proto_type.DESCRIPTOR.GetOptions().map_entry ): recursive_type = type(proto_type().value) diff --git a/packages/proto-plus/tests/test_message.py b/packages/proto-plus/tests/test_message.py index b2ba3986d840..fc57df26aa91 100644 --- a/packages/proto-plus/tests/test_message.py +++ b/packages/proto-plus/tests/test_message.py @@ -500,3 +500,36 @@ class Arm(proto.Message): def test_dir_message_base(): assert set(dir(proto.Message)) == set(dir(type)) + + +def test_invalid_initialization_type_error(): + """Verify that bad types passed to __init__ raise a descriptive TypeError.""" + class UserProfile(proto.Message): + username = proto.Field(proto.STRING, number=1) + age = proto.Field(proto.INT32, number=2) + + with pytest.raises(TypeError) as excinfo: + # Passing a list where a string is expected + UserProfile(username=["not", "a", "string"]) + + error_msg = str(excinfo.value) + assert "Failed to initialize UserProfile" in error_msg + assert "Underlying error" in error_msg + assert isinstance(excinfo.value.__cause__, TypeError) + + +def test_invalid_assignment_type_error(): + """Verify that bad types assigned via __setattr__ raise a descriptive TypeError.""" + class UserProfile(proto.Message): + username = proto.Field(proto.STRING, number=1) + age = proto.Field(proto.INT32, number=2) + profile = UserProfile() + + with pytest.raises(TypeError) as excinfo: + # Assigning a dictionary where an integer is expected + profile.age = {"invalid": "type"} + + error_msg = str(excinfo.value) + assert "Failed to set field 'age' on UserProfile" in error_msg + assert "{'invalid': 'type'}" in error_msg + assert isinstance(excinfo.value.__cause__, TypeError) \ No newline at end of file From 3d83a1908ac416262b02d83c8720e324d1e3b1f1 Mon Sep 17 00:00:00 2001 From: Arima Gupta Date: Thu, 16 Jul 2026 21:42:22 +0530 Subject: [PATCH 4/4] style: format proto-plus code with black --- packages/proto-plus/proto/message.py | 3 --- packages/proto-plus/tests/test_message.py | 5 ++++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/proto-plus/proto/message.py b/packages/proto-plus/proto/message.py index 29bca5218806..1eb36a977faa 100644 --- a/packages/proto-plus/proto/message.py +++ b/packages/proto-plus/proto/message.py @@ -688,7 +688,6 @@ def __init__( ) ) from e - def _get_pb_type_from_key(self, key): """Given a key, return the corresponding pb_type. @@ -877,8 +876,6 @@ def __setattr__(self, key, value): ) ) from e - - def __getstate__(self): """Serialize for pickling.""" return self._pb.SerializeToString() diff --git a/packages/proto-plus/tests/test_message.py b/packages/proto-plus/tests/test_message.py index fc57df26aa91..0649eae5ee9b 100644 --- a/packages/proto-plus/tests/test_message.py +++ b/packages/proto-plus/tests/test_message.py @@ -504,6 +504,7 @@ def test_dir_message_base(): def test_invalid_initialization_type_error(): """Verify that bad types passed to __init__ raise a descriptive TypeError.""" + class UserProfile(proto.Message): username = proto.Field(proto.STRING, number=1) age = proto.Field(proto.INT32, number=2) @@ -520,9 +521,11 @@ class UserProfile(proto.Message): def test_invalid_assignment_type_error(): """Verify that bad types assigned via __setattr__ raise a descriptive TypeError.""" + class UserProfile(proto.Message): username = proto.Field(proto.STRING, number=1) age = proto.Field(proto.INT32, number=2) + profile = UserProfile() with pytest.raises(TypeError) as excinfo: @@ -532,4 +535,4 @@ class UserProfile(proto.Message): error_msg = str(excinfo.value) assert "Failed to set field 'age' on UserProfile" in error_msg assert "{'invalid': 'type'}" in error_msg - assert isinstance(excinfo.value.__cause__, TypeError) \ No newline at end of file + assert isinstance(excinfo.value.__cause__, TypeError)