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 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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):
member_validator = _AndValidator(member_validator)
hynek marked this conversation as resolved.
Show resolved Hide resolved
return _DeepIterable(member_validator, iterable_validator)


Expand Down
2 changes: 1 addition & 1 deletion src/attr/validators.pyi
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]]],
hynek marked this conversation as resolved.
Show resolved Hide resolved
iterable_validator: Optional[_ValidatorType[_I]] = ...,
) -> _ValidatorType[_I]: ...
def deep_mapping(
Expand Down
73 changes: 53 additions & 20 deletions tests/test_validators.py
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)]]
)
hynek marked this conversation as resolved.
Show resolved Hide resolved
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)
vedantpuri marked this conversation as resolved.
Show resolved Hide resolved

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
)
)
vedantpuri marked this conversation as resolved.
Show resolved Hide resolved
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