Skip to content

Commit

Permalink
Allow deep_iterable member validator to accept a list of validators (
Browse files Browse the repository at this point in the history
…#925)

* Add Multiple Validators to deep iterable

* Add Tests + Fix some doc strings

* Update typing

* Limit this PR to only accepting a list of member validators

* Respond to PR comments

* Commit missing file

* Apply suggestions from code review

Co-authored-by: Hynek Schlawack <hs@ox.cx>

* Split other test too

* Fix CI + Remove Weird parens

* Use and_ instead of And_

* Add tuple to list of tests

Co-authored-by: Hynek Schlawack <hs@ox.cx>
  • Loading branch information
vedantpuri and hynek committed Mar 20, 2022
1 parent b754366 commit d0c73d8
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 13 deletions.
1 change: 1 addition & 0 deletions changelog.d/925.change.rst
@@ -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
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
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
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)


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

0 comments on commit d0c73d8

Please sign in to comment.