Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/attr/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,14 +407,16 @@ def deep_iterable(member_validator, iterable_validator=None):
"""
A validator that performs deep validation of an iterable.

:param member_validator: Validator to apply to iterable members
:param member_validator: Validator(s) to apply to iterable members
:param iterable_validator: Validator to apply to iterable itself
(optional)

.. versionadded:: 19.1.0

:raises TypeError: if any sub-validators fail
"""
if isinstance(member_validator, list):
member_validator = _AndValidator(member_validator)
return _DeepIterable(member_validator, iterable_validator)


Expand Down
2 changes: 1 addition & 1 deletion src/attr/validators.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def matches_re(
] = ...,
) -> _ValidatorType[AnyStr]: ...
def deep_iterable(
member_validator: _ValidatorType[_T],
member_validator: Union[_ValidatorType[_T], List[_ValidatorType[_T]]],
iterable_validator: Optional[_ValidatorType[_I]] = ...,
) -> _ValidatorType[_I]: ...
def deep_mapping(
Expand Down
73 changes: 53 additions & 20 deletions tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,21 +515,25 @@ def test_in_all(self):
"""
assert deep_iterable.__name__ in validator_module.__all__

def test_success_member_only(self):
@pytest.mark.parametrize(
"member_validator", [instance_of(int), [always_pass, instance_of(int)]]
)
def test_success_member_only(self, member_validator):
"""
If the member validator succeeds and the iterable validator is not set,
nothing happens.
"""
member_validator = instance_of(int)
v = deep_iterable(member_validator)
a = simple_attr("test")
v(None, a, [42])

def test_success_member_and_iterable(self):
@pytest.mark.parametrize(
"member_validator", [instance_of(int), [always_pass, instance_of(int)]]
)
def test_success_member_and_iterable(self, member_validator):
"""
If both the member and iterable validators succeed, nothing happens.
"""
member_validator = instance_of(int)
iterable_validator = instance_of(list)
v = deep_iterable(member_validator, iterable_validator)
a = simple_attr("test")
Expand All @@ -542,6 +546,8 @@ def test_success_member_and_iterable(self):
(42, instance_of(list)),
(42, 42),
(42, None),
([instance_of(int), 42], 42),
([42, instance_of(int)], 42),
),
)
def test_noncallable_validators(
Expand All @@ -562,17 +568,22 @@ def test_noncallable_validators(
assert message in e.value.msg
assert value == e.value.value

def test_fail_invalid_member(self):
@pytest.mark.parametrize(
"member_validator", [instance_of(int), [always_pass, instance_of(int)]]
)
def test_fail_invalid_member(self, member_validator):
"""
Raise member validator error if an invalid member is found.
"""
member_validator = instance_of(int)
v = deep_iterable(member_validator)
a = simple_attr("test")
with pytest.raises(TypeError):
v(None, a, [42, "42"])

def test_fail_invalid_iterable(self):
@pytest.mark.parametrize(
"member_validator", [instance_of(int), [always_pass, instance_of(int)]]
)
def test_fail_invalid_iterable(self, member_validator):
"""
Raise iterable validator error if an invalid iterable is found.
"""
Expand All @@ -583,42 +594,64 @@ def test_fail_invalid_iterable(self):
with pytest.raises(TypeError):
v(None, a, [42])

def test_fail_invalid_member_and_iterable(self):
@pytest.mark.parametrize(
"member_validator", [instance_of(int), [always_pass, instance_of(int)]]
)
def test_fail_invalid_member_and_iterable(self, member_validator):
"""
Raise iterable validator error if both the iterable
and a member are invalid.
"""
member_validator = instance_of(int)
iterable_validator = instance_of(tuple)
v = deep_iterable(member_validator, iterable_validator)
a = simple_attr("test")
with pytest.raises(TypeError):
v(None, a, [42, "42"])

def test_repr_member_only(self):
@pytest.mark.parametrize(
"member_validator", [instance_of(int), [always_pass, instance_of(int)]]
)
def test_repr_member_only(self, member_validator):
"""
Returned validator has a useful `__repr__`
when only member validator is set.
"""
member_validator = instance_of(int)
member_repr = "<instance_of validator for type <{type} 'int'>>".format(
type=TYPE
)
if isinstance(member_validator, list):
member_repr = (
"_AndValidator(_validators=[{func}, "
"<instance_of validator for type <{type} 'int'>>])"
).format(func=repr(always_pass), type=TYPE)
else:
member_repr = (
"<instance_of validator for type <{type} 'int'>>".format(
type=TYPE
)
)
v = deep_iterable(member_validator)
expected_repr = (
"<deep_iterable validator for iterables of {member_repr}>"
).format(member_repr=member_repr)
assert ((expected_repr)) == repr(v)

def test_repr_member_and_iterable(self):
@pytest.mark.parametrize(
"member_validator", [instance_of(int), [always_pass, instance_of(int)]]
)
def test_repr_member_and_iterable(self, member_validator):
"""
Returned validator has a useful `__repr__` when both member
and iterable validators are set.
"""
member_validator = instance_of(int)
member_repr = "<instance_of validator for type <{type} 'int'>>".format(
type=TYPE
)
if isinstance(member_validator, list):
member_repr = (
"_AndValidator(_validators=[{func}, "
"<instance_of validator for type <{type} 'int'>>])"
).format(func=repr(always_pass), type=TYPE)
else:
member_repr = (
"<instance_of validator for type <{type} 'int'>>".format(
type=TYPE
)
)
iterable_validator = instance_of(list)
iterable_repr = (
"<instance_of validator for type <{type} 'list'>>"
Expand Down Expand Up @@ -804,7 +837,7 @@ def test_hashability():

class TestLtLeGeGt:
"""
Tests for `max_len`.
Tests for `Lt, Le, Ge, Gt`.
"""

BOUND = 4
Expand Down