Skip to content

Commit 6ae0a52

Browse files
committed
Add input type validation to max_len and min_len validators to prevent crashes on non-sized objects
1 parent 3823062 commit 6ae0a52

3 files changed

Lines changed: 60 additions & 3 deletions

File tree

src/attr/_compat.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import sys
66
import threading
77

8-
from collections.abc import Mapping, Sequence # noqa: F401
8+
from collections.abc import Mapping, Sequence, Sized # noqa: F401
99
from typing import _GenericAlias
1010

1111

@@ -97,3 +97,4 @@ def get_generic_base(cl):
9797
if cl.__class__ is _GenericAlias:
9898
return cl.__origin__
9999
return None
100+

src/attr/validators.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from contextlib import contextmanager
1111
from re import Pattern
1212

13+
from ._compat import Mapping, Sized
1314
from ._config import get_run_validators, set_run_validators
1415
from ._make import _AndValidator, and_, attrib, attrs
1516
from .converters import default_if_none
@@ -397,6 +398,15 @@ def __call__(self, inst, attr, value):
397398
if self.mapping_validator is not None:
398399
self.mapping_validator(inst, attr, value)
399400

401+
if not isinstance(value, Mapping):
402+
msg = f"'{attr.name}' must be a mapping (got {value!r} that is a {value.__class__!r})."
403+
raise TypeError(
404+
msg,
405+
attr,
406+
Mapping,
407+
value,
408+
)
409+
400410
for key in value:
401411
if self.key_validator is not None:
402412
self.key_validator(inst, attr, key)
@@ -543,6 +553,9 @@ def __call__(self, inst, attr, value):
543553
"""
544554
We use a callable class to be able to change the ``__repr__``.
545555
"""
556+
if not isinstance(value, Sized):
557+
msg = f"'{attr.name}' must be a sized object (got {value!r} that is a {value.__class__!r})."
558+
raise TypeError(msg)
546559
if len(value) > self.max_length:
547560
msg = f"Length of '{attr.name}' must be <= {self.max_length}: {len(value)}"
548561
raise ValueError(msg)
@@ -572,6 +585,9 @@ def __call__(self, inst, attr, value):
572585
"""
573586
We use a callable class to be able to change the ``__repr__``.
574587
"""
588+
if not isinstance(value, Sized):
589+
msg = f"'{attr.name}' must be a sized object (got {value!r} that is a {value.__class__!r})."
590+
raise TypeError(msg)
575591
if len(value) < self.min_length:
576592
msg = f"Length of '{attr.name}' must be >= {self.min_length}: {len(value)}"
577593
raise ValueError(msg)
@@ -747,4 +763,4 @@ def or_(*validators):
747763
for v in validators:
748764
vals.extend(v.validators if isinstance(v, _OrValidator) else [v])
749765

750-
return _OrValidator(tuple(vals))
766+
return _OrValidator(tuple(vals))

tests/test_validators.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,20 @@ def test_validators_iterables(self, conv):
807807
assert and_(*value_validator) == v.value_validator
808808
assert and_(*mapping_validator) == v.mapping_validator
809809

810+
def test_fail_non_mapping(self):
811+
"""
812+
Raise TypeError if value is not a mapping.
813+
"""
814+
key_validator = instance_of(str)
815+
value_validator = instance_of(int)
816+
v = deep_mapping(key_validator, value_validator)
817+
a = simple_attr("test")
818+
with pytest.raises(TypeError) as e:
819+
v(None, a, [1, 2, 3])
820+
821+
msg = f"'{a.name}' must be a mapping (got [1, 2, 3] that is a {list!r})."
822+
assert msg in str(e.value)
823+
810824

811825
class TestIsCallable:
812826
"""
@@ -1023,6 +1037,19 @@ def test_repr(self):
10231037
"""
10241038
assert repr(max_len(23)) == "<max_len validator for 23>"
10251039

1040+
def test_fail_non_sized(self):
1041+
"""
1042+
Raise TypeError if value is not a sized object (e.g., generator).
1043+
"""
1044+
@attr.s
1045+
class Tester:
1046+
value = attr.ib(validator=max_len(self.MAX_LENGTH))
1047+
1048+
with pytest.raises(TypeError) as e:
1049+
Tester((x for x in range(2)))
1050+
1051+
assert "must be a sized object" in str(e.value)
1052+
10261053

10271054
class TestMinLen:
10281055
"""
@@ -1094,6 +1121,19 @@ def test_repr(self):
10941121
"""
10951122
assert repr(min_len(23)) == "<min_len validator for 23>"
10961123

1124+
def test_fail_non_sized(self):
1125+
"""
1126+
Raise TypeError if value is not a sized object (e.g., generator).
1127+
"""
1128+
@attr.s
1129+
class Tester:
1130+
value = attr.ib(validator=min_len(self.MIN_LENGTH))
1131+
1132+
with pytest.raises(TypeError) as e:
1133+
Tester((x for x in range(2)))
1134+
1135+
assert "must be a sized object" in str(e.value)
1136+
10971137

10981138
class TestSubclassOf:
10991139
"""
@@ -1396,4 +1436,4 @@ def test_repr(self):
13961436
assert (
13971437
"<or validator wrapping (<instance_of validator for type "
13981438
"<class 'int'>>, <instance_of validator for type <class 'str'>>)>"
1399-
) == repr(v)
1439+
) == repr(v)

0 commit comments

Comments
 (0)