Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
1 change: 1 addition & 0 deletions changelog.d/925.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow the `member_validator` of the `deep_iterable` validator to accept a list of validators
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
3 changes: 2 additions & 1 deletion src/attr/validators.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ from typing import (
)

from . import _ValidatorType
from . import _ValidatorArgType

_T = TypeVar("_T")
_T1 = TypeVar("_T1")
Expand Down Expand Up @@ -62,7 +63,7 @@ def matches_re(
] = ...,
) -> _ValidatorType[AnyStr]: ...
def deep_iterable(
member_validator: _ValidatorType[_T],
member_validator: _ValidatorArgType[T],
iterable_validator: Optional[_ValidatorType[_I]] = ...,
) -> _ValidatorType[_I]: ...
def deep_mapping(
Expand Down
69 changes: 54 additions & 15 deletions tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,18 @@ def test_repr(self):
assert (("<in_ validator with options [3, 4, 5]>")) == repr(v)


@pytest.fixture(
name="member_validator",
params=(instance_of(int), [always_pass, instance_of(int)]),
scope="module",
)
def _member_validator(request):
"""
Provides sample `member_validator`s for some tests in `TestDeepIterable`
"""
return request.param


class TestDeepIterable(object):
"""
Tests for `deep_iterable`.
Expand All @@ -515,21 +527,19 @@ def test_in_all(self):
"""
assert deep_iterable.__name__ in validator_module.__all__

def test_success_member_only(self):
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):
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 +552,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 +574,16 @@ def test_noncallable_validators(
assert message in e.value.msg
assert value == e.value.value

def test_fail_invalid_member(self):
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):
def test_fail_invalid_iterable(self, member_validator):
"""
Raise iterable validator error if an invalid iterable is found.
"""
Expand All @@ -583,27 +594,33 @@ def test_fail_invalid_iterable(self):
with pytest.raises(TypeError):
v(None, a, [42])

def test_fail_invalid_member_and_iterable(self):
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):
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}>"
Expand All @@ -630,6 +647,28 @@ def test_repr_member_and_iterable(self):
).format(iterable_repr=iterable_repr, member_repr=member_repr)
assert expected_repr == repr(v)

def test_repr_sequence_member_and_iterable(self):
"""
Returned validator has a useful `__repr__` when both member
and iterable validators are set and the member validator is a list of
validators
"""
member_validator = [always_pass, instance_of(int)]
member_repr = (
"_AndValidator(_validators=[{func}, "
"<instance_of validator for type <{type} 'int'>>])"
).format(func=repr(always_pass), type=TYPE)
iterable_validator = instance_of(list)
iterable_repr = (
"<instance_of validator for type <{type} 'list'>>"
).format(type=TYPE)
v = deep_iterable(member_validator, iterable_validator)
expected_repr = (
"<deep_iterable validator for"
" {iterable_repr} iterables of {member_repr}>"
).format(iterable_repr=iterable_repr, member_repr=member_repr)
assert expected_repr == repr(v)


class TestDeepMapping(object):
"""
Expand Down Expand Up @@ -804,7 +843,7 @@ def test_hashability():

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

BOUND = 4
Expand Down