diff --git a/src/attr/validators.py b/src/attr/validators.py index 3408abef5..837e003b6 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -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) @@ -409,12 +415,12 @@ 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 @@ -422,6 +428,10 @@ def deep_mapping( *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. @@ -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) diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index 94ea31fff..3b38e5980 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -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]: ... diff --git a/tests/test_validators.py b/tests/test_validators.py index df94d2aa6..9fd3f2dcb 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -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: """ @@ -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: """ diff --git a/tests/typing_example.py b/tests/typing_example.py index e5738aa77..89fccff3b 100644 --- a/tests/typing_example.py +++ b/tests/typing_example.py @@ -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( @@ -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)