From d0c73d8faa184cdff8d6a0378ed5b85ca57cde7d Mon Sep 17 00:00:00 2001 From: Vedant Puri Date: Sun, 20 Mar 2022 04:48:55 -0400 Subject: [PATCH] Allow `deep_iterable` member validator to accept a list of validators (#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 * 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 --- changelog.d/925.change.rst | 1 + src/attr/validators.py | 4 +- src/attr/validators.pyi | 3 +- tests/test_validators.py | 76 ++++++++++++++++++++++++++++++++------ 4 files changed, 71 insertions(+), 13 deletions(-) create mode 100644 changelog.d/925.change.rst 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