Skip to content
56 changes: 52 additions & 4 deletions src/attr/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,8 +343,24 @@ def __call__(self, inst, attr, value):
if self.iterable_validator is not None:
self.iterable_validator(inst, attr, value)

for member in value:
self.member_validator(inst, attr, member)
for idx, member in enumerate(value):
try:
self.member_validator(inst, attr, member)
except Exception as e: # noqa: PERF203
orig_msg = e.args[0] if e.args else str(e)
if (
isinstance(orig_msg, str)
and "'" + attr.name + "'" in orig_msg
):
index_name = attr.name + "[" + str(idx) + "]"
new_msg = orig_msg.replace(
"'" + attr.name + "'",
"'" + index_name + "'",
1,
)
new_args = (new_msg, *e.args[1:])
raise type(e)(*new_args).with_traceback(None) from None
raise

def __repr__(self):
iterable_identifier = (
Expand Down Expand Up @@ -399,9 +415,41 @@ def __call__(self, inst, attr, value):

for key in value:
if self.key_validator is not None:
self.key_validator(inst, attr, key)
try:
self.key_validator(inst, attr, key)
except Exception as e:
orig_msg = e.args[0] if e.args else str(e)
if (
isinstance(orig_msg, str)
and "'" + attr.name + "'" in orig_msg
):
index_name = attr.name + "[key:" + repr(key) + "]"
new_msg = orig_msg.replace(
"'" + attr.name + "'",
"'" + index_name + "'",
1,
)
new_args = (new_msg, *e.args[1:])
raise type(e)(*new_args).with_traceback(None) from None
raise
if self.value_validator is not None:
self.value_validator(inst, attr, value[key])
try:
self.value_validator(inst, attr, value[key])
except Exception as e:
orig_msg = e.args[0] if e.args else str(e)
if (
isinstance(orig_msg, str)
and "'" + attr.name + "'" in orig_msg
):
index_name = attr.name + "[" + repr(key) + "]"
new_msg = orig_msg.replace(
"'" + attr.name + "'",
"'" + index_name + "'",
1,
)
new_args = (new_msg, *e.args[1:])
raise type(e)(*new_args).with_traceback(None) from None
raise

def __repr__(self):
return f"<deep_mapping validator for objects mapping {self.key_validator!r} to {self.value_validator!r}>"
Expand Down
47 changes: 46 additions & 1 deletion tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,21 @@ def test_validators_iterables(self, conv):
assert and_(*member_validator) == v.member_validator
assert and_(*iterable_validator) == v.iterable_validator

def test_member_validator_error_without_attr_name(self):
"""
If a member validator raises an exception whose message does not
contain the attr name, the original exception is re-raised unchanged.
"""

def _validator(inst, attr, value):
raise ValueError("something went wrong")

v = deep_iterable(_validator)
a = simple_attr("test")

with pytest.raises(ValueError, match="something went wrong"):
v(None, a, ["ok", "bad"])


class TestDeepMapping:
"""
Expand Down Expand Up @@ -807,6 +822,36 @@ def test_validators_iterables(self, conv):
assert and_(*value_validator) == v.value_validator
assert and_(*mapping_validator) == v.mapping_validator

def test_key_validator_error_without_attr_name(self):
"""
If a key validator raises an exception whose message does not
contain the attr name, the original exception is re-raised unchanged.
"""

def _key_validator(inst, attr, value):
raise TypeError("bad key type")

v = deep_mapping(key_validator=_key_validator)
a = simple_attr("test")

with pytest.raises(TypeError, match="bad key type"):
v(None, a, {"bad_key": 1})

def test_value_validator_error_without_attr_name(self):
"""
If a value validator raises an exception whose message does not
contain the attr name, the original exception is re-raised unchanged.
"""

def _value_validator(inst, attr, value):
raise TypeError("bad value type")

v = deep_mapping(value_validator=_value_validator)
a = simple_attr("test")

with pytest.raises(TypeError, match="bad value type"):
v(None, a, {"a": "bad_value"})


class TestIsCallable:
"""
Expand Down Expand Up @@ -1359,7 +1404,7 @@ def test_bad_exception_args(self):
not_(wrapped, exc_types=(str, int))

assert (
"'exc_types' must be a subclass of <class 'Exception'> "
"'exc_types[0]' must be a subclass of <class 'Exception'> "
"(got <class 'str'>)."
) == e.value.args[0]

Expand Down