Skip to content

Commit 5bab46d

Browse files
authored
Allow deep_iterable and deep_mapping take lists/tuples (#1449)
* Allow deep_iterable and deep_mapping take lists/tuples Fixes #1243 * Fix docstring
1 parent 000c563 commit 5bab46d

File tree

4 files changed

+77
-13
lines changed

4 files changed

+77
-13
lines changed

src/attr/validators.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -361,18 +361,24 @@ def deep_iterable(member_validator, iterable_validator=None):
361361
A validator that performs deep validation of an iterable.
362362
363363
Args:
364-
member_validator: Validator to apply to iterable members.
364+
member_validator: Validator(s) to apply to iterable members.
365365
366366
iterable_validator:
367-
Validator to apply to iterable itself (optional).
367+
Validator(s) to apply to iterable itself (optional).
368368
369369
Raises
370370
TypeError: if any sub-validators fail
371371
372372
.. versionadded:: 19.1.0
373+
374+
.. versionchanged:: 25.4.0
375+
*member_validator* and *iterable_validator* can now be a list or tuple
376+
of validators.
373377
"""
374378
if isinstance(member_validator, (list, tuple)):
375379
member_validator = and_(*member_validator)
380+
if isinstance(iterable_validator, (list, tuple)):
381+
iterable_validator = and_(*iterable_validator)
376382
return _DeepIterable(member_validator, iterable_validator)
377383

378384

@@ -409,19 +415,23 @@ def deep_mapping(
409415
*value_validator* must be provided.
410416
411417
Args:
412-
key_validator: Validator to apply to dictionary keys.
418+
key_validator: Validator(s) to apply to dictionary keys.
413419
414-
value_validator: Validator to apply to dictionary values.
420+
value_validator: Validator(s) to apply to dictionary values.
415421
416422
mapping_validator:
417-
Validator to apply to top-level mapping attribute.
423+
Validator(s) to apply to top-level mapping attribute.
418424
419425
.. versionadded:: 19.1.0
420426
421427
.. versionchanged:: 25.4.0
422428
*key_validator* and *value_validator* are now optional, but at least one
423429
of them must be provided.
424430
431+
.. versionchanged:: 25.4.0
432+
*key_validator*, *value_validator*, and *mapping_validator* can now be a
433+
list or tuple of validators.
434+
425435
Raises:
426436
TypeError: If any sub-validator fails on validation.
427437
@@ -435,6 +445,13 @@ def deep_mapping(
435445
)
436446
raise ValueError(msg)
437447

448+
if isinstance(key_validator, (list, tuple)):
449+
key_validator = and_(*key_validator)
450+
if isinstance(value_validator, (list, tuple)):
451+
value_validator = and_(*value_validator)
452+
if isinstance(mapping_validator, (list, tuple)):
453+
mapping_validator = and_(*mapping_validator)
454+
438455
return _DeepMapping(key_validator, value_validator, mapping_validator)
439456

440457

src/attr/validators.pyi

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,19 +63,19 @@ def matches_re(
6363
) -> _ValidatorType[AnyStr]: ...
6464
def deep_iterable(
6565
member_validator: _ValidatorArgType[_T],
66-
iterable_validator: _ValidatorType[_I] | None = ...,
66+
iterable_validator: _ValidatorArgType[_I] | None = ...,
6767
) -> _ValidatorType[_I]: ...
6868
@overload
6969
def deep_mapping(
70-
key_validator: _ValidatorType[_K],
71-
value_validator: _ValidatorType[_V] | None = ...,
72-
mapping_validator: _ValidatorType[_M] | None = ...,
70+
key_validator: _ValidatorArgType[_K],
71+
value_validator: _ValidatorArgType[_V] | None = ...,
72+
mapping_validator: _ValidatorArgType[_M] | None = ...,
7373
) -> _ValidatorType[_M]: ...
7474
@overload
7575
def deep_mapping(
76-
key_validator: _ValidatorType[_K] | None = ...,
77-
value_validator: _ValidatorType[_V] = ...,
78-
mapping_validator: _ValidatorType[_M] | None = ...,
76+
key_validator: _ValidatorArgType[_K] | None = ...,
77+
value_validator: _ValidatorArgType[_V] = ...,
78+
mapping_validator: _ValidatorArgType[_M] | None = ...,
7979
) -> _ValidatorType[_M]: ...
8080
def is_callable() -> _ValidatorType[_T]: ...
8181
def lt(val: _T) -> _ValidatorType[_T]: ...

tests/test_validators.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,19 @@ def test_repr_sequence_member_and_iterable(self):
621621

622622
assert expected_repr == repr(v)
623623

624+
@pytest.mark.parametrize("conv", [list, tuple])
625+
def test_validators_iterables(self, conv):
626+
"""
627+
If iterables are passed as validators, they are combined with and_.
628+
"""
629+
member_validator = (instance_of(int),)
630+
iterable_validator = (instance_of(list), min_len(1))
631+
632+
v = deep_iterable(conv(member_validator), conv(iterable_validator))
633+
634+
assert and_(*member_validator) == v.member_validator
635+
assert and_(*iterable_validator) == v.iterable_validator
636+
624637

625638
class TestDeepMapping:
626639
"""
@@ -755,6 +768,25 @@ def test_value_validator_can_be_none(self):
755768

756769
v(None, a, {"a": 6, "b": 7})
757770

771+
@pytest.mark.parametrize("conv", [list, tuple])
772+
def test_validators_iterables(self, conv):
773+
"""
774+
If iterables are passed as validators, they are combined with and_.
775+
"""
776+
key_validator = (instance_of(str), min_len(2))
777+
value_validator = (instance_of(int), ge(10))
778+
mapping_validator = (instance_of(dict), max_len(2))
779+
780+
v = deep_mapping(
781+
conv(key_validator),
782+
conv(value_validator),
783+
conv(mapping_validator),
784+
)
785+
786+
assert and_(*key_validator) == v.key_validator
787+
assert and_(*value_validator) == v.value_validator
788+
assert and_(*mapping_validator) == v.mapping_validator
789+
758790

759791
class TestIsCallable:
760792
"""

tests/typing_example.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,19 @@ class Validated:
190190
attr.validators.instance_of(C), attr.validators.instance_of(list)
191191
),
192192
)
193-
aa = attr.ib(
193+
a2 = attr.ib(
194194
type=Tuple[C],
195195
validator=attr.validators.deep_iterable(
196196
attr.validators.instance_of(C), attr.validators.instance_of(tuple)
197197
),
198198
)
199+
a3 = attr.ib(
200+
type=Tuple[C],
201+
validator=attr.validators.deep_iterable(
202+
[attr.validators.instance_of(C)],
203+
[attr.validators.instance_of(tuple)],
204+
),
205+
)
199206
b = attr.ib(
200207
type=List[C],
201208
validator=attr.validators.deep_iterable(
@@ -226,6 +233,14 @@ class Validated:
226233
value_validator=attr.validators.instance_of(C)
227234
),
228235
)
236+
d4 = attr.ib(
237+
type=Dict[C, D],
238+
validator=attr.validators.deep_mapping(
239+
key_validator=[attr.validators.instance_of(C)],
240+
value_validator=[attr.validators.instance_of(C)],
241+
mapping_validator=[attr.validators.instance_of(dict)],
242+
),
243+
)
229244
e: str = attr.ib(validator=attr.validators.matches_re(re.compile(r"foo")))
230245
f: str = attr.ib(
231246
validator=attr.validators.matches_re(r"foo", flags=42, func=re.search)

0 commit comments

Comments
 (0)