diff --git a/src/attr/validators.py b/src/attr/validators.py index 0b1a29443..459971422 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -10,6 +10,7 @@ from contextlib import contextmanager from re import Pattern +from ._compat import Mapping from ._config import get_run_validators, set_run_validators from ._make import _AndValidator, and_, attrib, attrs from .converters import default_if_none @@ -397,6 +398,15 @@ def __call__(self, inst, attr, value): if self.mapping_validator is not None: self.mapping_validator(inst, attr, value) + if not isinstance(value, Mapping): + msg = f"'{attr.name}' must be a mapping (got {value!r} that is a {value.__class__!r})." + raise TypeError( + msg, + attr, + Mapping, + value, + ) + for key in value: if self.key_validator is not None: self.key_validator(inst, attr, key) diff --git a/tests/test_validators.py b/tests/test_validators.py index 8caa64272..e4b4b9eff 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -807,6 +807,22 @@ def test_validators_iterables(self, conv): assert and_(*value_validator) == v.value_validator assert and_(*mapping_validator) == v.mapping_validator + def test_fail_non_mapping(self): + """ + Raise TypeError if value is not a mapping. + """ + key_validator = instance_of(str) + value_validator = instance_of(int) + v = deep_mapping(key_validator, value_validator) + a = simple_attr("test") + with pytest.raises(TypeError) as e: + v(None, a, [1, 2, 3]) + + msg = ( + f"'{a.name}' must be a mapping (got [1, 2, 3] that is a {list!r})." + ) + assert msg in str(e.value) + class TestIsCallable: """