From 20a7a64be5256ae210ad1cab2594db24a20da5d3 Mon Sep 17 00:00:00 2001 From: Vedant Puri Date: Thu, 3 Mar 2022 22:42:44 -0500 Subject: [PATCH 01/11] Add Multiple Validators to deep iterable --- src/attr/validators.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/attr/validators.py b/src/attr/validators.py index 7e3ff1635..ce3330916 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -415,6 +415,10 @@ def deep_iterable(member_validator, iterable_validator=None): :raises TypeError: if any sub-validators fail """ + if isinstance(member_validator, list): + member_validator = _AndValidator(member_validator) + if iterable_validator and isinstance(iterable_validator, list): + iterable_validator = _AndValidator(iterable_validator) return _DeepIterable(member_validator, iterable_validator) From afd759766f272005f5a9cfc2b683b3c5db5f7056 Mon Sep 17 00:00:00 2001 From: Vedant Puri Date: Sat, 5 Mar 2022 13:46:01 -0500 Subject: [PATCH 02/11] Add Tests + Fix some doc strings --- src/attr/validators.py | 4 +-- tests/test_validators.py | 73 +++++++++++++++++++++++++++++----------- 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/src/attr/validators.py b/src/attr/validators.py index ce3330916..c7939fc6b 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -407,8 +407,8 @@ 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 iterable_validator: Validator to apply to iterable itself + :param member_validator: Validator(s) to apply to iterable members + :param iterable_validator: Validator(s) to apply to iterable itself (optional) .. versionadded:: 19.1.0 diff --git a/tests/test_validators.py b/tests/test_validators.py index 69ec16456..bb3410b88 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -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)]] + ) + 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") @@ -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( @@ -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. """ @@ -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 = ">".format( - type=TYPE - ) + if isinstance(member_validator, list): + member_repr = ( + "_AndValidator(_validators=[{func}, " + ">])" + ).format(func=repr(always_pass), type=TYPE) + else: + member_repr = ( + ">".format( + 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): + @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 = ">".format( - type=TYPE - ) + if isinstance(member_validator, list): + member_repr = ( + "_AndValidator(_validators=[{func}, " + ">])" + ).format(func=repr(always_pass), type=TYPE) + else: + member_repr = ( + ">".format( + type=TYPE + ) + ) iterable_validator = instance_of(list) iterable_repr = ( ">" @@ -804,7 +837,7 @@ def test_hashability(): class TestLtLeGeGt: """ - Tests for `max_len`. + Tests for `Lt, Le, Ge, Gt`. """ BOUND = 4 From 60b3a9f37165aafcfa1b8a307e71084012e2754a Mon Sep 17 00:00:00 2001 From: Vedant Puri Date: Sat, 5 Mar 2022 13:53:04 -0500 Subject: [PATCH 03/11] Update typing --- src/attr/validators.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index 81b9910f5..0edb1854b 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -62,7 +62,7 @@ def matches_re( ] = ..., ) -> _ValidatorType[AnyStr]: ... def deep_iterable( - member_validator: _ValidatorType[_T], + member_validator: Union[_ValidatorType[_T], List[_ValidatorType[_T]]], iterable_validator: Optional[_ValidatorType[_I]] = ..., ) -> _ValidatorType[_I]: ... def deep_mapping( From b4ecfa21514dd60b64bc5a2ba182e009eb21e8f5 Mon Sep 17 00:00:00 2001 From: Vedant Puri Date: Sun, 6 Mar 2022 11:46:13 -0500 Subject: [PATCH 04/11] Limit this PR to only accepting a list of member validators --- src/attr/validators.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/attr/validators.py b/src/attr/validators.py index c7939fc6b..41a2fb411 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -408,7 +408,7 @@ def deep_iterable(member_validator, iterable_validator=None): A validator that performs deep validation of an iterable. :param member_validator: Validator(s) to apply to iterable members - :param iterable_validator: Validator(s) to apply to iterable itself + :param iterable_validator: Validator to apply to iterable itself (optional) .. versionadded:: 19.1.0 @@ -417,8 +417,6 @@ def deep_iterable(member_validator, iterable_validator=None): """ if isinstance(member_validator, list): member_validator = _AndValidator(member_validator) - if iterable_validator and isinstance(iterable_validator, list): - iterable_validator = _AndValidator(iterable_validator) return _DeepIterable(member_validator, iterable_validator) From 5f9446bfa5c43e1a7907488cee1d26f608f263d6 Mon Sep 17 00:00:00 2001 From: Vedant Puri Date: Tue, 15 Mar 2022 18:54:05 -0400 Subject: [PATCH 05/11] Respond to PR comments --- src/attr/validators.pyi | 3 +- tests/test_validators.py | 72 ++++++++++++++++++++++------------------ 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index 0edb1854b..fb534f1bf 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: Union[_ValidatorType[_T], List[_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 bb3410b88..61d3b5a2e 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -504,6 +504,18 @@ def test_repr(self): assert (("")) == repr(v) +@pytest.fixture( + name="member_validator", + params=(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,9 +527,6 @@ def test_in_all(self): """ assert deep_iterable.__name__ in validator_module.__all__ - @pytest.mark.parametrize( - "member_validator", [instance_of(int), [always_pass, instance_of(int)]] - ) def test_success_member_only(self, member_validator): """ If the member validator succeeds and the iterable validator is not set, @@ -527,9 +536,6 @@ def test_success_member_only(self, member_validator): a = simple_attr("test") v(None, a, [42]) - @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. @@ -568,9 +574,6 @@ def test_noncallable_validators( assert message in e.value.msg assert value == e.value.value - @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. @@ -580,9 +583,6 @@ def test_fail_invalid_member(self, member_validator): with pytest.raises(TypeError): v(None, a, [42, "42"]) - @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. @@ -594,9 +594,6 @@ def test_fail_invalid_iterable(self, member_validator): with pytest.raises(TypeError): v(None, a, [42]) - @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 @@ -608,9 +605,6 @@ def test_fail_invalid_member_and_iterable(self, member_validator): with pytest.raises(TypeError): v(None, a, [42, "42"]) - @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__` @@ -633,25 +627,37 @@ def test_repr_member_only(self, member_validator): ).format(member_repr=member_repr) assert ((expected_repr)) == repr(v) - @pytest.mark.parametrize( - "member_validator", [instance_of(int), [always_pass, instance_of(int)]] - ) - def test_repr_member_and_iterable(self, member_validator): + def test_repr_member_and_iterable(self): """ Returned validator has a useful `__repr__` when both member and iterable validators are set. """ - if isinstance(member_validator, list): - member_repr = ( - "_AndValidator(_validators=[{func}, " - ">])" - ).format(func=repr(always_pass), type=TYPE) - else: - member_repr = ( - ">".format( - type=TYPE - ) - ) + member_validator = instance_of(int) + member_repr = ">".format( + 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) + + 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 = ( ">" From 6e2c524b9ce6ae84653e02a55888d33a0b411ae4 Mon Sep 17 00:00:00 2001 From: Vedant Puri Date: Tue, 15 Mar 2022 18:56:21 -0400 Subject: [PATCH 06/11] Commit missing file --- changelog.d/925.change.rst | 1 + 1 file changed, 1 insertion(+) 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..6416bd7df --- /dev/null +++ b/changelog.d/925.change.rst @@ -0,0 +1 @@ +Allow the `member_validator` of the `deep_iterable` validator to accept a list of validators From fd5777ef819defe6ed8bae4c4be928e5f6a4ebfa Mon Sep 17 00:00:00 2001 From: Vedant Puri Date: Wed, 16 Mar 2022 09:30:02 -0400 Subject: [PATCH 07/11] Apply suggestions from code review Co-authored-by: Hynek Schlawack --- changelog.d/925.change.rst | 2 +- tests/test_validators.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.d/925.change.rst b/changelog.d/925.change.rst index 6416bd7df..ddc795248 100644 --- a/changelog.d/925.change.rst +++ b/changelog.d/925.change.rst @@ -1 +1 @@ -Allow the `member_validator` of the `deep_iterable` validator to accept a list of validators +``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/tests/test_validators.py b/tests/test_validators.py index 61d3b5a2e..8be66c984 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -667,6 +667,7 @@ def test_repr_sequence_member_and_iterable(self): "" ).format(iterable_repr=iterable_repr, member_repr=member_repr) + assert expected_repr == repr(v) From a21d8e6017f02cf16d5e8ec4fe587184da472c38 Mon Sep 17 00:00:00 2001 From: Vedant Puri Date: Wed, 16 Mar 2022 09:39:21 -0400 Subject: [PATCH 08/11] Split other test too --- tests/test_validators.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/tests/test_validators.py b/tests/test_validators.py index 61d3b5a2e..905cc1cd9 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -605,22 +605,32 @@ def test_fail_invalid_member_and_iterable(self, member_validator): with pytest.raises(TypeError): v(None, a, [42, "42"]) - def test_repr_member_only(self, member_validator): + def test_repr_member_only(self): """ Returned validator has a useful `__repr__` when only member validator is set. """ - if isinstance(member_validator, list): - member_repr = ( - "_AndValidator(_validators=[{func}, " - ">])" - ).format(func=repr(always_pass), type=TYPE) - else: - member_repr = ( - ">".format( - type=TYPE - ) - ) + member_validator = instance_of(int) + member_repr = ">".format( + type=TYPE + ) + v = deep_iterable(member_validator) + expected_repr = ( + "" + ).format(member_repr=member_repr) + 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 = ( "" From 6fc6f5d0a11ebfae8c54517cee620354a7d10d6a Mon Sep 17 00:00:00 2001 From: Vedant Puri Date: Thu, 17 Mar 2022 21:21:42 -0400 Subject: [PATCH 09/11] Fix CI + Remove Weird parens --- src/attr/validators.pyi | 2 +- tests/test_validators.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index fb534f1bf..54b9dba24 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -63,7 +63,7 @@ def matches_re( ] = ..., ) -> _ValidatorType[AnyStr]: ... def deep_iterable( - member_validator: _ValidatorArgType[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 516727af8..8ae1a9bf6 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -618,7 +618,7 @@ 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): """ @@ -635,7 +635,7 @@ def test_repr_member_only_sequence(self): expected_repr = ( "" ).format(member_repr=member_repr) - assert ((expected_repr)) == repr(v) + assert expected_repr == repr(v) def test_repr_member_and_iterable(self): """ From 62e2e6a779b1c822b565e3c06e5ee377b6d7ab9a Mon Sep 17 00:00:00 2001 From: Vedant Puri Date: Sat, 19 Mar 2022 15:57:59 -0400 Subject: [PATCH 10/11] Use and_ instead of And_ --- src/attr/validators.py | 4 ++-- tests/test_validators.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/attr/validators.py b/src/attr/validators.py index 41a2fb411..5f850cc8f 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -415,8 +415,8 @@ def deep_iterable(member_validator, iterable_validator=None): :raises TypeError: if any sub-validators fail """ - if isinstance(member_validator, list): - member_validator = _AndValidator(member_validator) + if isinstance(member_validator, (list, tuple)): + member_validator = and_(*member_validator) return _DeepIterable(member_validator, iterable_validator) diff --git a/tests/test_validators.py b/tests/test_validators.py index 8ae1a9bf6..8db755fd2 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -628,8 +628,8 @@ def test_repr_member_only_sequence(self): """ member_validator = [always_pass, instance_of(int)] member_repr = ( - "_AndValidator(_validators=[{func}, " - ">])" + "_AndValidator(_validators=({func}, " + ">))" ).format(func=repr(always_pass), type=TYPE) v = deep_iterable(member_validator) expected_repr = ( @@ -665,8 +665,8 @@ def test_repr_sequence_member_and_iterable(self): """ member_validator = [always_pass, instance_of(int)] member_repr = ( - "_AndValidator(_validators=[{func}, " - ">])" + "_AndValidator(_validators=({func}, " + ">))" ).format(func=repr(always_pass), type=TYPE) iterable_validator = instance_of(list) iterable_repr = ( From c7933ecf30bac182c33896d04edbe6b0012ae235 Mon Sep 17 00:00:00 2001 From: Vedant Puri Date: Sat, 19 Mar 2022 19:53:49 -0400 Subject: [PATCH 11/11] Add tuple to list of tests --- tests/test_validators.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_validators.py b/tests/test_validators.py index 8db755fd2..fce774b10 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -506,7 +506,11 @@ def test_repr(self): @pytest.fixture( name="member_validator", - params=(instance_of(int), [always_pass, instance_of(int)]), + params=( + instance_of(int), + [always_pass, instance_of(int)], + (always_pass, instance_of(int)), + ), scope="module", ) def _member_validator(request):