Skip to content

Commit d0c73d8

Browse files
vedantpurihynek
andauthored
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 <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>
1 parent b754366 commit d0c73d8

4 files changed

Lines changed: 71 additions & 13 deletions

File tree

changelog.d/925.change.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
``attrs.validators.deep_iterable()``'s *member_validator* argument now also accepts a list of validators and wraps them in an ``attrs.validators.and_()``.

src/attr/validators.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,14 +407,16 @@ def deep_iterable(member_validator, iterable_validator=None):
407407
"""
408408
A validator that performs deep validation of an iterable.
409409
410-
:param member_validator: Validator to apply to iterable members
410+
:param member_validator: Validator(s) to apply to iterable members
411411
:param iterable_validator: Validator to apply to iterable itself
412412
(optional)
413413
414414
.. versionadded:: 19.1.0
415415
416416
:raises TypeError: if any sub-validators fail
417417
"""
418+
if isinstance(member_validator, (list, tuple)):
419+
member_validator = and_(*member_validator)
418420
return _DeepIterable(member_validator, iterable_validator)
419421

420422

src/attr/validators.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ from typing import (
1818
)
1919

2020
from . import _ValidatorType
21+
from . import _ValidatorArgType
2122

2223
_T = TypeVar("_T")
2324
_T1 = TypeVar("_T1")
@@ -62,7 +63,7 @@ def matches_re(
6263
] = ...,
6364
) -> _ValidatorType[AnyStr]: ...
6465
def deep_iterable(
65-
member_validator: _ValidatorType[_T],
66+
member_validator: _ValidatorArgType[_T],
6667
iterable_validator: Optional[_ValidatorType[_I]] = ...,
6768
) -> _ValidatorType[_I]: ...
6869
def deep_mapping(

tests/test_validators.py

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,22 @@ def test_repr(self):
504504
assert (("<in_ validator with options [3, 4, 5]>")) == repr(v)
505505

506506

507+
@pytest.fixture(
508+
name="member_validator",
509+
params=(
510+
instance_of(int),
511+
[always_pass, instance_of(int)],
512+
(always_pass, instance_of(int)),
513+
),
514+
scope="module",
515+
)
516+
def _member_validator(request):
517+
"""
518+
Provides sample `member_validator`s for some tests in `TestDeepIterable`
519+
"""
520+
return request.param
521+
522+
507523
class TestDeepIterable(object):
508524
"""
509525
Tests for `deep_iterable`.
@@ -515,21 +531,19 @@ def test_in_all(self):
515531
"""
516532
assert deep_iterable.__name__ in validator_module.__all__
517533

518-
def test_success_member_only(self):
534+
def test_success_member_only(self, member_validator):
519535
"""
520536
If the member validator succeeds and the iterable validator is not set,
521537
nothing happens.
522538
"""
523-
member_validator = instance_of(int)
524539
v = deep_iterable(member_validator)
525540
a = simple_attr("test")
526541
v(None, a, [42])
527542

528-
def test_success_member_and_iterable(self):
543+
def test_success_member_and_iterable(self, member_validator):
529544
"""
530545
If both the member and iterable validators succeed, nothing happens.
531546
"""
532-
member_validator = instance_of(int)
533547
iterable_validator = instance_of(list)
534548
v = deep_iterable(member_validator, iterable_validator)
535549
a = simple_attr("test")
@@ -542,6 +556,8 @@ def test_success_member_and_iterable(self):
542556
(42, instance_of(list)),
543557
(42, 42),
544558
(42, None),
559+
([instance_of(int), 42], 42),
560+
([42, instance_of(int)], 42),
545561
),
546562
)
547563
def test_noncallable_validators(
@@ -562,17 +578,16 @@ def test_noncallable_validators(
562578
assert message in e.value.msg
563579
assert value == e.value.value
564580

565-
def test_fail_invalid_member(self):
581+
def test_fail_invalid_member(self, member_validator):
566582
"""
567583
Raise member validator error if an invalid member is found.
568584
"""
569-
member_validator = instance_of(int)
570585
v = deep_iterable(member_validator)
571586
a = simple_attr("test")
572587
with pytest.raises(TypeError):
573588
v(None, a, [42, "42"])
574589

575-
def test_fail_invalid_iterable(self):
590+
def test_fail_invalid_iterable(self, member_validator):
576591
"""
577592
Raise iterable validator error if an invalid iterable is found.
578593
"""
@@ -583,12 +598,11 @@ def test_fail_invalid_iterable(self):
583598
with pytest.raises(TypeError):
584599
v(None, a, [42])
585600

586-
def test_fail_invalid_member_and_iterable(self):
601+
def test_fail_invalid_member_and_iterable(self, member_validator):
587602
"""
588603
Raise iterable validator error if both the iterable
589604
and a member are invalid.
590605
"""
591-
member_validator = instance_of(int)
592606
iterable_validator = instance_of(tuple)
593607
v = deep_iterable(member_validator, iterable_validator)
594608
a = simple_attr("test")
@@ -608,7 +622,24 @@ def test_repr_member_only(self):
608622
expected_repr = (
609623
"<deep_iterable validator for iterables of {member_repr}>"
610624
).format(member_repr=member_repr)
611-
assert ((expected_repr)) == repr(v)
625+
assert expected_repr == repr(v)
626+
627+
def test_repr_member_only_sequence(self):
628+
"""
629+
Returned validator has a useful `__repr__`
630+
when only member validator is set and the member validator is a list of
631+
validators
632+
"""
633+
member_validator = [always_pass, instance_of(int)]
634+
member_repr = (
635+
"_AndValidator(_validators=({func}, "
636+
"<instance_of validator for type <{type} 'int'>>))"
637+
).format(func=repr(always_pass), type=TYPE)
638+
v = deep_iterable(member_validator)
639+
expected_repr = (
640+
"<deep_iterable validator for iterables of {member_repr}>"
641+
).format(member_repr=member_repr)
642+
assert expected_repr == repr(v)
612643

613644
def test_repr_member_and_iterable(self):
614645
"""
@@ -630,6 +661,29 @@ def test_repr_member_and_iterable(self):
630661
).format(iterable_repr=iterable_repr, member_repr=member_repr)
631662
assert expected_repr == repr(v)
632663

664+
def test_repr_sequence_member_and_iterable(self):
665+
"""
666+
Returned validator has a useful `__repr__` when both member
667+
and iterable validators are set and the member validator is a list of
668+
validators
669+
"""
670+
member_validator = [always_pass, instance_of(int)]
671+
member_repr = (
672+
"_AndValidator(_validators=({func}, "
673+
"<instance_of validator for type <{type} 'int'>>))"
674+
).format(func=repr(always_pass), type=TYPE)
675+
iterable_validator = instance_of(list)
676+
iterable_repr = (
677+
"<instance_of validator for type <{type} 'list'>>"
678+
).format(type=TYPE)
679+
v = deep_iterable(member_validator, iterable_validator)
680+
expected_repr = (
681+
"<deep_iterable validator for"
682+
" {iterable_repr} iterables of {member_repr}>"
683+
).format(iterable_repr=iterable_repr, member_repr=member_repr)
684+
685+
assert expected_repr == repr(v)
686+
633687

634688
class TestDeepMapping(object):
635689
"""
@@ -804,7 +858,7 @@ def test_hashability():
804858

805859
class TestLtLeGeGt:
806860
"""
807-
Tests for `max_len`.
861+
Tests for `Lt, Le, Ge, Gt`.
808862
"""
809863

810864
BOUND = 4

0 commit comments

Comments
 (0)