Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow deep_iterable member validator to accept a list of validators #925

Merged
merged 17 commits into from
Mar 20, 2022
Merged
Show file tree
Hide file tree
Changes from all 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 @@
``attrs.validators.deep_iterable()``'s *member_validator* argument now also accepts a list of validators and wraps them in an ``attrs.validators.and_()``.
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, tuple)):
member_validator = and_(*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
76 changes: 65 additions & 11 deletions tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,22 @@ 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)],
(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 +531,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 +556,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 +578,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,12 +598,11 @@ 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")
Expand All @@ -608,7 +622,24 @@ def test_repr_member_only(self):
expected_repr = (
"<deep_iterable validator for iterables of {member_repr}>"
).format(member_repr=member_repr)
assert ((expected_repr)) == repr(v)
assert expected_repr == repr(v)

def test_repr_member_only_sequence(self):
"""
Returned validator has a useful `__repr__`
when only member validator is 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)
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):
"""
Expand All @@ -630,6 +661,29 @@ 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)
vedantpuri marked this conversation as resolved.
Show resolved Hide resolved


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

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

BOUND = 4
Expand Down