Skip to content
3 changes: 2 additions & 1 deletion packages/proto-plus/proto/marshal/marshal.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,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)
Expand Down
59 changes: 36 additions & 23 deletions packages/proto-plus/proto/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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."""
Expand Down
36 changes: 36 additions & 0 deletions packages/proto-plus/tests/test_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading