Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions src/attr/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,18 +361,24 @@ def deep_iterable(member_validator, iterable_validator=None):
A validator that performs deep validation of an iterable.

Args:
member_validator: Validator to apply to iterable members.
member_validator: Validator(s) to apply to iterable members.

iterable_validator:
Validator to apply to iterable itself (optional).
Validator(s) to apply to iterable itself (optional).

Raises
TypeError: if any sub-validators fail

.. versionadded:: 19.1.0

.. versionchanged:: 25.4.0
*member_validator* and *iterable_validator* can now be a list or tuple
of validators.
"""
if isinstance(member_validator, (list, tuple)):
member_validator = and_(*member_validator)
if isinstance(iterable_validator, (list, tuple)):
iterable_validator = and_(*iterable_validator)
return _DeepIterable(member_validator, iterable_validator)


Expand Down Expand Up @@ -409,19 +415,23 @@ def deep_mapping(
*value_validator* must be provided.

Args:
key_validator: Validator to apply to dictionary keys.
key_validator: Validator(s) to apply to dictionary keys.

value_validator: Validator to apply to dictionary values.
value_validator: Validator(s) to apply to dictionary values.

mapping_validator:
Validator to apply to top-level mapping attribute.
Validator(s) to apply to top-level mapping attribute.

.. versionadded:: 19.1.0

.. versionchanged:: 25.4.0
*key_validator* and *value_validator* are now optional, but at least one
of them must be provided.

.. versionchanged:: 25.4.0
*key_validator*, *value_validator*, and *mapping_validator* can now be a
list or tuple of validators.

Raises:
TypeError: If any sub-validator fails on validation.

Expand All @@ -435,6 +445,13 @@ def deep_mapping(
)
raise ValueError(msg)

if isinstance(key_validator, (list, tuple)):
key_validator = and_(*key_validator)
if isinstance(value_validator, (list, tuple)):
value_validator = and_(*value_validator)
if isinstance(mapping_validator, (list, tuple)):
mapping_validator = and_(*mapping_validator)

return _DeepMapping(key_validator, value_validator, mapping_validator)


Expand Down
14 changes: 7 additions & 7 deletions src/attr/validators.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,19 @@ def matches_re(
) -> _ValidatorType[AnyStr]: ...
def deep_iterable(
member_validator: _ValidatorArgType[_T],
iterable_validator: _ValidatorType[_I] | None = ...,
iterable_validator: _ValidatorArgType[_I] | None = ...,
) -> _ValidatorType[_I]: ...
@overload
def deep_mapping(
key_validator: _ValidatorType[_K],
value_validator: _ValidatorType[_V] | None = ...,
mapping_validator: _ValidatorType[_M] | None = ...,
key_validator: _ValidatorArgType[_K],
value_validator: _ValidatorArgType[_V] | None = ...,
mapping_validator: _ValidatorArgType[_M] | None = ...,
) -> _ValidatorType[_M]: ...
@overload
def deep_mapping(
key_validator: _ValidatorType[_K] | None = ...,
value_validator: _ValidatorType[_V] = ...,
mapping_validator: _ValidatorType[_M] | None = ...,
key_validator: _ValidatorArgType[_K] | None = ...,
value_validator: _ValidatorArgType[_V] = ...,
mapping_validator: _ValidatorArgType[_M] | None = ...,
) -> _ValidatorType[_M]: ...
def is_callable() -> _ValidatorType[_T]: ...
def lt(val: _T) -> _ValidatorType[_T]: ...
Expand Down
32 changes: 32 additions & 0 deletions tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,19 @@ def test_repr_sequence_member_and_iterable(self):

assert expected_repr == repr(v)

@pytest.mark.parametrize("conv", [list, tuple])
def test_validators_iterables(self, conv):
"""
If iterables are passed as validators, they are combined with and_.
"""
member_validator = (instance_of(int),)
iterable_validator = (instance_of(list), min_len(1))

v = deep_iterable(conv(member_validator), conv(iterable_validator))

assert and_(*member_validator) == v.member_validator
assert and_(*iterable_validator) == v.iterable_validator


class TestDeepMapping:
"""
Expand Down Expand Up @@ -755,6 +768,25 @@ def test_value_validator_can_be_none(self):

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

@pytest.mark.parametrize("conv", [list, tuple])
def test_validators_iterables(self, conv):
"""
If iterables are passed as validators, they are combined with and_.
"""
key_validator = (instance_of(str), min_len(2))
value_validator = (instance_of(int), ge(10))
mapping_validator = (instance_of(dict), max_len(2))

v = deep_mapping(
conv(key_validator),
conv(value_validator),
conv(mapping_validator),
)

assert and_(*key_validator) == v.key_validator
assert and_(*value_validator) == v.value_validator
assert and_(*mapping_validator) == v.mapping_validator


class TestIsCallable:
"""
Expand Down
17 changes: 16 additions & 1 deletion tests/typing_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,19 @@ class Validated:
attr.validators.instance_of(C), attr.validators.instance_of(list)
),
)
aa = attr.ib(
a2 = attr.ib(
type=Tuple[C],
validator=attr.validators.deep_iterable(
attr.validators.instance_of(C), attr.validators.instance_of(tuple)
),
)
a3 = attr.ib(
type=Tuple[C],
validator=attr.validators.deep_iterable(
[attr.validators.instance_of(C)],
[attr.validators.instance_of(tuple)],
),
)
b = attr.ib(
type=List[C],
validator=attr.validators.deep_iterable(
Expand Down Expand Up @@ -226,6 +233,14 @@ class Validated:
value_validator=attr.validators.instance_of(C)
),
)
d4 = attr.ib(
type=Dict[C, D],
validator=attr.validators.deep_mapping(
key_validator=[attr.validators.instance_of(C)],
value_validator=[attr.validators.instance_of(C)],
mapping_validator=[attr.validators.instance_of(dict)],
),
)
e: str = attr.ib(validator=attr.validators.matches_re(re.compile(r"foo")))
f: str = attr.ib(
validator=attr.validators.matches_re(r"foo", flags=42, func=re.search)
Expand Down