diff --git a/changelog.d/925.change.rst b/changelog.d/925.change.rst new file mode 100644 index 000000000..ddc795248 --- /dev/null +++ b/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_()``. diff --git a/src/attr/validators.py b/src/attr/validators.py index 7e3ff1635..5f850cc8f 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -407,7 +407,7 @@ 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) @@ -415,6 +415,8 @@ def deep_iterable(member_validator, iterable_validator=None): :raises TypeError: if any sub-validators fail """ + if isinstance(member_validator, (list, tuple)): + member_validator = and_(*member_validator) return _DeepIterable(member_validator, iterable_validator) diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index 81b9910f5..54b9dba24 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -18,6 +18,7 @@ from typing import ( ) from . import _ValidatorType +from . import _ValidatorArgType _T = TypeVar("_T") _T1 = TypeVar("_T1") @@ -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( diff --git a/tests/test_validators.py b/tests/test_validators.py index 69ec16456..fce774b10 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -504,6 +504,22 @@ def test_repr(self): assert (("")) == 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`. @@ -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") @@ -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( @@ -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. """ @@ -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") @@ -608,7 +622,24 @@ def test_repr_member_only(self): expected_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}, " + ">))" + ).format(func=repr(always_pass), type=TYPE) + v = deep_iterable(member_validator) + expected_repr = ( + "" + ).format(member_repr=member_repr) + assert expected_repr == repr(v) def test_repr_member_and_iterable(self): """ @@ -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}, " + ">))" + ).format(func=repr(always_pass), type=TYPE) + iterable_validator = instance_of(list) + iterable_repr = ( + ">" + ).format(type=TYPE) + v = deep_iterable(member_validator, iterable_validator) + expected_repr = ( + "" + ).format(iterable_repr=iterable_repr, member_repr=member_repr) + + assert expected_repr == repr(v) + class TestDeepMapping(object): """ @@ -804,7 +858,7 @@ def test_hashability(): class TestLtLeGeGt: """ - Tests for `max_len`. + Tests for `Lt, Le, Ge, Gt`. """ BOUND = 4