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/proto/message.py b/packages/proto-plus/proto/message.py index 123e58a1c5fe..1eb36a977faa 100644 --- a/packages/proto-plus/proto/message.py +++ b/packages/proto-plus/proto/message.py @@ -659,28 +659,34 @@ 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. - super().__setattr__("_pb", self._meta.pb(**params)) + # Create the internal protocol buffer. + super().__setattr__("_pb", self._meta.pb(**params)) + except TypeError as e: + raise TypeError( + "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. @@ -852,16 +858,23 @@ 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: - self._pb.MergeFrom(self._meta.pb(**{key: pb_value})) + # Merge in the value being set. + if pb_value is not None: + self._pb.MergeFrom(self._meta.pb(**{key: pb_value})) + 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): """Serialize for pickling.""" diff --git a/packages/proto-plus/tests/test_message.py b/packages/proto-plus/tests/test_message.py index b2ba3986d840..0649eae5ee9b 100644 --- a/packages/proto-plus/tests/test_message.py +++ b/packages/proto-plus/tests/test_message.py @@ -500,3 +500,39 @@ 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)