| # SPDX-License-Identifier: MIT | |
| """ | |
| Tests for `attr.validators`. | |
| """ | |
| import re | |
| import sys | |
| import pytest | |
| import attr | |
| from attr import _config, fields, has | |
| from attr import validators as validator_module | |
| from attr.validators import ( | |
| _subclass_of, | |
| and_, | |
| deep_iterable, | |
| deep_mapping, | |
| ge, | |
| gt, | |
| in_, | |
| instance_of, | |
| is_callable, | |
| le, | |
| lt, | |
| matches_re, | |
| max_len, | |
| min_len, | |
| ne, | |
| not_, | |
| optional, | |
| or_, | |
| ) | |
| from .utils import simple_attr | |
| class TestDisableValidators: | |
| @pytest.fixture(autouse=True) | |
| def _reset_default(self): | |
| """ | |
| Make sure validators are always enabled after a test. | |
| """ | |
| yield | |
| _config._run_validators = True | |
| def test_default(self): | |
| """ | |
| Run validators by default. | |
| """ | |
| assert _config._run_validators is True | |
| @pytest.mark.parametrize( | |
| ("value", "expected"), [(True, False), (False, True)] | |
| ) | |
| def test_set_validators_disabled(self, value, expected): | |
| """ | |
| Sets `_run_validators`. | |
| """ | |
| validator_module.set_disabled(value) | |
| assert _config._run_validators is expected | |
| @pytest.mark.parametrize( | |
| ("value", "expected"), [(True, False), (False, True)] | |
| ) | |
| def test_disabled(self, value, expected): | |
| """ | |
| Returns `_run_validators`. | |
| """ | |
| _config._run_validators = value | |
| assert validator_module.get_disabled() is expected | |
| def test_disabled_ctx(self): | |
| """ | |
| The `disabled` context manager disables running validators, | |
| but only within its context. | |
| """ | |
| assert _config._run_validators is True | |
| with validator_module.disabled(): | |
| assert _config._run_validators is False | |
| assert _config._run_validators is True | |
| def test_disabled_ctx_with_errors(self): | |
| """ | |
| Running validators is re-enabled even if an error is raised. | |
| """ | |
| assert _config._run_validators is True | |
| with pytest.raises(ValueError), validator_module.disabled(): | |
| assert _config._run_validators is False | |
| raise ValueError("haha!") | |
| assert _config._run_validators is True | |
| def test_disabled_ctx_nested(self): | |
| """ | |
| Nested contextmanagers restore correct state. | |
| """ | |
| assert _config._run_validators is True | |
| with validator_module.disabled(): | |
| assert _config._run_validators is False | |
| with validator_module.disabled(): | |
| assert _config._run_validators is False | |
| assert _config._run_validators is False | |
| assert _config._run_validators is True | |
| class TestInstanceOf: | |
| """ | |
| Tests for `instance_of`. | |
| """ | |
| def test_in_all(self): | |
| """ | |
| Verify that this validator is in ``__all__``. | |
| """ | |
| assert instance_of.__name__ in validator_module.__all__ | |
| def test_success(self): | |
| """ | |
| Nothing happens if types match. | |
| """ | |
| v = instance_of(int) | |
| v(None, simple_attr("test"), 42) | |
| def test_subclass(self): | |
| """ | |
| Subclasses are accepted too. | |
| """ | |
| v = instance_of(int) | |
| # yep, bools are a subclass of int :( | |
| v(None, simple_attr("test"), True) | |
| def test_fail(self): | |
| """ | |
| Raises `TypeError` on wrong types. | |
| """ | |
| v = instance_of(int) | |
| a = simple_attr("test") | |
| with pytest.raises(TypeError) as e: | |
| v(None, a, "42") | |
| assert ( | |
| "'test' must be <class 'int'> (got '42' that is a <class 'str'>).", | |
| a, | |
| int, | |
| "42", | |
| ) == e.value.args | |
| def test_repr(self): | |
| """ | |
| Returned validator has a useful `__repr__`. | |
| """ | |
| v = instance_of(int) | |
| assert ("<instance_of validator for type <class 'int'>>") == repr(v) | |
| class TestMatchesRe: | |
| """ | |
| Tests for `matches_re`. | |
| """ | |
| def test_in_all(self): | |
| """ | |
| validator is in ``__all__``. | |
| """ | |
| assert matches_re.__name__ in validator_module.__all__ | |
| def test_match(self): | |
| """ | |
| Silent on matches, raises ValueError on mismatches. | |
| """ | |
| @attr.s | |
| class ReTester: | |
| str_match = attr.ib(validator=matches_re("a|ab")) | |
| ReTester("ab") # shouldn't raise exceptions | |
| with pytest.raises(TypeError): | |
| ReTester(1) | |
| with pytest.raises(ValueError): | |
| ReTester("1") | |
| with pytest.raises(ValueError): | |
| ReTester("a1") | |
| def test_flags(self): | |
| """ | |
| Flags are propagated to the match function. | |
| """ | |
| @attr.s | |
| class MatchTester: | |
| val = attr.ib(validator=matches_re("a", re.IGNORECASE, re.match)) | |
| MatchTester("A1") # test flags and using re.match | |
| def test_precompiled_pattern(self): | |
| """ | |
| Pre-compiled patterns are accepted. | |
| """ | |
| pattern = re.compile("a") | |
| @attr.s | |
| class RePatternTester: | |
| val = attr.ib(validator=matches_re(pattern)) | |
| RePatternTester("a") | |
| def test_precompiled_pattern_no_flags(self): | |
| """ | |
| A pre-compiled pattern cannot be combined with a 'flags' argument. | |
| """ | |
| pattern = re.compile("") | |
| with pytest.raises( | |
| TypeError, match="can only be used with a string pattern" | |
| ): | |
| matches_re(pattern, flags=re.IGNORECASE) | |
| def test_different_func(self): | |
| """ | |
| Changing the match functions works. | |
| """ | |
| @attr.s | |
| class SearchTester: | |
| val = attr.ib(validator=matches_re("a", 0, re.search)) | |
| SearchTester("bab") # re.search will match | |
| def test_catches_invalid_func(self): | |
| """ | |
| Invalid match functions are caught. | |
| """ | |
| with pytest.raises(ValueError) as ei: | |
| matches_re("a", 0, lambda: None) | |
| if sys.version_info >= (3, 15): | |
| errmsg = ( | |
| "'func' must be one of None, fullmatch, prefixmatch, search." | |
| ) | |
| else: | |
| errmsg = "'func' must be one of None, fullmatch, match, search." | |
| assert errmsg == ei.value.args[0] | |
| @pytest.mark.parametrize( | |
| "func", [None, getattr(re, "fullmatch", None), re.match, re.search] | |
| ) | |
| def test_accepts_all_valid_func(self, func): | |
| """ | |
| Every valid match function is accepted. | |
| """ | |
| matches_re("a", func=func) | |
| def test_repr(self): | |
| """ | |
| __repr__ is meaningful. | |
| """ | |
| assert repr(matches_re("a")).startswith( | |
| "<matches_re validator for pattern" | |
| ) | |
| def always_pass(_, __, ___): | |
| """ | |
| Toy validator that always passes. | |
| """ | |
| def always_fail(_, __, ___): | |
| """ | |
| Toy validator that always fails. | |
| """ | |
| 0 / 0 | |
| class TestAnd: | |
| def test_in_all(self): | |
| """ | |
| Verify that this validator is in ``__all__``. | |
| """ | |
| assert and_.__name__ in validator_module.__all__ | |
| def test_success(self): | |
| """ | |
| Succeeds if all wrapped validators succeed. | |
| """ | |
| v = and_(instance_of(int), always_pass) | |
| v(None, simple_attr("test"), 42) | |
| def test_fail(self): | |
| """ | |
| Fails if any wrapped validator fails. | |
| """ | |
| v = and_(instance_of(int), always_fail) | |
| with pytest.raises(ZeroDivisionError): | |
| v(None, simple_attr("test"), 42) | |
| def test_sugar(self): | |
| """ | |
| `and_(v1, v2, v3)` and `[v1, v2, v3]` are equivalent. | |
| """ | |
| @attr.s | |
| class C: | |
| a1 = attr.ib("a1", validator=and_(instance_of(int))) | |
| a2 = attr.ib("a2", validator=[instance_of(int)]) | |
| assert C.__attrs_attrs__[0].validator == C.__attrs_attrs__[1].validator | |
| @pytest.mark.parametrize( | |
| "validator", | |
| [ | |
| instance_of(int), | |
| [always_pass, instance_of(int)], | |
| (always_pass, instance_of(int)), | |
| ], | |
| ) | |
| class TestOptional: | |
| """ | |
| Tests for `optional`. | |
| """ | |
| def test_in_all(self, validator): | |
| """ | |
| Verify that this validator is in ``__all__``. | |
| """ | |
| assert optional.__name__ in validator_module.__all__ | |
| def test_success(self, validator): | |
| """ | |
| Nothing happens if validator succeeds. | |
| """ | |
| v = optional(validator) | |
| v(None, simple_attr("test"), 42) | |
| def test_success_with_none(self, validator): | |
| """ | |
| Nothing happens if None. | |
| """ | |
| v = optional(validator) | |
| v(None, simple_attr("test"), None) | |
| def test_fail(self, validator): | |
| """ | |
| Raises `TypeError` on wrong types. | |
| """ | |
| v = optional(validator) | |
| a = simple_attr("test") | |
| with pytest.raises(TypeError) as e: | |
| v(None, a, "42") | |
| assert ( | |
| "'test' must be <class 'int'> (got '42' that is a <class 'str'>).", | |
| a, | |
| int, | |
| "42", | |
| ) == e.value.args | |
| def test_repr(self, validator): | |
| """ | |
| Returned validator has a useful `__repr__`. | |
| """ | |
| v = optional(validator) | |
| if isinstance(validator, list): | |
| repr_s = ( | |
| f"<optional validator for _AndValidator(_validators=[{always_pass!r}, " | |
| "<instance_of validator for type <class 'int'>>]) or None>" | |
| ) | |
| elif isinstance(validator, tuple): | |
| repr_s = ( | |
| f"<optional validator for _AndValidator(_validators=({always_pass!r}, " | |
| "<instance_of validator for type <class 'int'>>)) or None>" | |
| ) | |
| else: | |
| repr_s = ( | |
| "<optional validator for <instance_of validator for type " | |
| "<class 'int'>> or None>" | |
| ) | |
| assert repr_s == repr(v) | |
| class TestIn_: | |
| """ | |
| Tests for `in_`. | |
| """ | |
| def test_in_all(self): | |
| """ | |
| Verify that this validator is in ``__all__``. | |
| """ | |
| assert in_.__name__ in validator_module.__all__ | |
| def test_success_with_value(self): | |
| """ | |
| If the value is in our options, nothing happens. | |
| """ | |
| v = in_([1, 2, 3]) | |
| a = simple_attr("test") | |
| v(1, a, 3) | |
| def test_fail(self): | |
| """ | |
| Raise ValueError if the value is outside our options. | |
| """ | |
| v = in_([1, 2, 3]) | |
| a = simple_attr("test") | |
| with pytest.raises(ValueError) as e: | |
| v(None, a, None) | |
| assert ( | |
| "'test' must be in [1, 2, 3] (got None)", | |
| a, | |
| [1, 2, 3], | |
| None, | |
| ) == e.value.args | |
| def test_fail_with_string(self): | |
| """ | |
| Raise ValueError if the value is outside our options when the | |
| options are specified as a string and the value is not a string. | |
| """ | |
| v = in_("abc") | |
| a = simple_attr("test") | |
| with pytest.raises(ValueError) as e: | |
| v(None, a, None) | |
| assert ( | |
| "'test' must be in 'abc' (got None)", | |
| a, | |
| "abc", | |
| None, | |
| ) == e.value.args | |
| def test_repr(self): | |
| """ | |
| Returned validator has a useful `__repr__`. | |
| """ | |
| v = in_([3, 4, 5]) | |
| assert ("<in_ validator with options [3, 4, 5]>") == repr(v) | |
| def test_is_hashable(self): | |
| """ | |
| `in_` is hashable, so fields using it can be used with the include and | |
| exclude filters. | |
| """ | |
| @attr.s | |
| class C: | |
| x: int = attr.ib(validator=attr.validators.in_({1, 2})) | |
| i = C(2) | |
| attr.asdict(i, filter=attr.filters.include(lambda val: True)) | |
| attr.asdict(i, filter=attr.filters.exclude(lambda val: True)) | |
| @pytest.fixture( | |
| name="member_validator", | |
| params=( | |
| instance_of(int), | |
| [always_pass, 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: | |
| """ | |
| Tests for `deep_iterable`. | |
| """ | |
| def test_in_all(self): | |
| """ | |
| Verify that this validator is in ``__all__``. | |
| """ | |
| assert deep_iterable.__name__ in validator_module.__all__ | |
| def test_success_member_only(self, member_validator): | |
| """ | |
| If the member validator succeeds and the iterable validator is not set, | |
| nothing happens. | |
| """ | |
| v = deep_iterable(member_validator) | |
| a = simple_attr("test") | |
| v(None, a, [42]) | |
| def test_success_member_and_iterable(self, member_validator): | |
| """ | |
| If both the member and iterable validators succeed, nothing happens. | |
| """ | |
| iterable_validator = instance_of(list) | |
| v = deep_iterable(member_validator, iterable_validator) | |
| a = simple_attr("test") | |
| v(None, a, [42]) | |
| @pytest.mark.parametrize( | |
| ("member_validator", "iterable_validator"), | |
| [ | |
| (instance_of(int), 42), | |
| (42, instance_of(list)), | |
| (42, 42), | |
| (42, None), | |
| ([instance_of(int), 42], 42), | |
| ([42, instance_of(int)], 42), | |
| ], | |
| ) | |
| def test_noncallable_validators( | |
| self, member_validator, iterable_validator | |
| ): | |
| """ | |
| Raise `TypeError` if any validators are not callable. | |
| """ | |
| with pytest.raises(TypeError) as e: | |
| deep_iterable(member_validator, iterable_validator) | |
| value = 42 | |
| message = ( | |
| f"must be callable (got {value} that is a {value.__class__})." | |
| ) | |
| assert message in e.value.args[0] | |
| assert value == e.value.args[1] | |
| assert message in e.value.msg | |
| assert value == e.value.value | |
| def test_fail_invalid_member(self, member_validator): | |
| """ | |
| Raise member validator error if an invalid member is found. | |
| """ | |
| v = deep_iterable(member_validator) | |
| a = simple_attr("test") | |
| with pytest.raises(TypeError): | |
| v(None, a, [42, "42"]) | |
| def test_fail_invalid_iterable(self, member_validator): | |
| """ | |
| Raise iterable validator error if an invalid iterable is found. | |
| """ | |
| 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]) | |
| def test_fail_invalid_member_and_iterable(self, member_validator): | |
| """ | |
| Raise iterable validator error if both the iterable | |
| and a member are invalid. | |
| """ | |
| 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): | |
| """ | |
| Returned validator has a useful `__repr__` | |
| when only member validator is set. | |
| """ | |
| member_validator = instance_of(int) | |
| member_repr = "<instance_of validator for type <class 'int'>>" | |
| v = deep_iterable(member_validator) | |
| expected_repr = ( | |
| f"<deep_iterable validator for iterables of {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 = ( | |
| f"_AndValidator(_validators=({always_pass!r}, " | |
| "<instance_of validator for type <class 'int'>>))" | |
| ) | |
| v = deep_iterable(member_validator) | |
| expected_repr = ( | |
| f"<deep_iterable validator for iterables of {member_repr}>" | |
| ) | |
| assert expected_repr == repr(v) | |
| def test_repr_member_and_iterable(self): | |
| """ | |
| Returned validator has a useful `__repr__` when both member | |
| and iterable validators are set. | |
| """ | |
| member_validator = instance_of(int) | |
| member_repr = "<instance_of validator for type <class 'int'>>" | |
| iterable_validator = instance_of(list) | |
| iterable_repr = "<instance_of validator for type <class 'list'>>" | |
| v = deep_iterable(member_validator, iterable_validator) | |
| expected_repr = ( | |
| "<deep_iterable validator for" | |
| f" {iterable_repr} iterables of {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 = ( | |
| f"_AndValidator(_validators=({always_pass!r}, " | |
| "<instance_of validator for type <class 'int'>>))" | |
| ) | |
| iterable_validator = instance_of(list) | |
| iterable_repr = "<instance_of validator for type <class 'list'>>" | |
| v = deep_iterable(member_validator, iterable_validator) | |
| expected_repr = ( | |
| "<deep_iterable validator for" | |
| f" {iterable_repr} iterables of {member_repr}>" | |
| ) | |
| 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: | |
| """ | |
| Tests for `deep_mapping`. | |
| """ | |
| def test_in_all(self): | |
| """ | |
| Verify that this validator is in ``__all__``. | |
| """ | |
| assert deep_mapping.__name__ in validator_module.__all__ | |
| def test_success(self): | |
| """ | |
| If both the key and value validators succeed, nothing happens. | |
| """ | |
| key_validator = instance_of(str) | |
| value_validator = instance_of(int) | |
| v = deep_mapping(key_validator, value_validator) | |
| a = simple_attr("test") | |
| v(None, a, {"a": 6, "b": 7}) | |
| @pytest.mark.parametrize( | |
| ("key_validator", "value_validator", "mapping_validator"), | |
| [ | |
| (42, instance_of(int), None), | |
| (instance_of(str), 42, None), | |
| (instance_of(str), instance_of(int), 42), | |
| (42, 42, None), | |
| (42, 42, 42), | |
| (42, None, None), | |
| (None, 42, None), | |
| ], | |
| ) | |
| def test_noncallable_validators( | |
| self, key_validator, value_validator, mapping_validator | |
| ): | |
| """ | |
| Raise `TypeError` if any validators are not callable. | |
| """ | |
| with pytest.raises(TypeError) as e: | |
| deep_mapping(key_validator, value_validator, mapping_validator) | |
| value = 42 | |
| message = ( | |
| f"must be callable (got {value} that is a {value.__class__})." | |
| ) | |
| assert message in e.value.args[0] | |
| assert value == e.value.args[1] | |
| assert message in e.value.msg | |
| assert value == e.value.value | |
| def test_fail_invalid_mapping(self): | |
| """ | |
| Raise `TypeError` if mapping validator fails. | |
| """ | |
| key_validator = instance_of(str) | |
| value_validator = instance_of(int) | |
| mapping_validator = instance_of(dict) | |
| v = deep_mapping(key_validator, value_validator, mapping_validator) | |
| a = simple_attr("test") | |
| with pytest.raises(TypeError): | |
| v(None, a, None) | |
| def test_fail_invalid_key(self): | |
| """ | |
| Raise key validator error if an invalid key is found. | |
| """ | |
| key_validator = instance_of(str) | |
| value_validator = instance_of(int) | |
| v = deep_mapping(key_validator, value_validator) | |
| a = simple_attr("test") | |
| with pytest.raises(TypeError): | |
| v(None, a, {"a": 6, 42: 7}) | |
| def test_fail_invalid_member(self): | |
| """ | |
| Raise key validator error if an invalid member value is found. | |
| """ | |
| key_validator = instance_of(str) | |
| value_validator = instance_of(int) | |
| v = deep_mapping(key_validator, value_validator) | |
| a = simple_attr("test") | |
| with pytest.raises(TypeError): | |
| v(None, a, {"a": "6", "b": 7}) | |
| def test_repr(self): | |
| """ | |
| Returned validator has a useful `__repr__`. | |
| """ | |
| key_validator = instance_of(str) | |
| key_repr = "<instance_of validator for type <class 'str'>>" | |
| value_validator = instance_of(int) | |
| value_repr = "<instance_of validator for type <class 'int'>>" | |
| v = deep_mapping(key_validator, value_validator) | |
| expected_repr = ( | |
| "<deep_mapping validator for objects mapping " | |
| f"{key_repr} to {value_repr}>" | |
| ) | |
| assert expected_repr == repr(v) | |
| def test_error_neither_validator_provided(self): | |
| """ | |
| Raise ValueError if neither key_validator nor value_validator is | |
| provided. | |
| """ | |
| with pytest.raises(ValueError) as e: | |
| deep_mapping() | |
| assert ( | |
| "At least one of key_validator or value_validator must be provided" | |
| == e.value.args[0] | |
| ) | |
| def test_key_validator_can_be_none(self): | |
| """ | |
| The key validator can be None. | |
| """ | |
| v = deep_mapping(value_validator=instance_of(int)) | |
| a = simple_attr("test") | |
| v(None, a, {"a": 6, "b": 7}) | |
| def test_value_validator_can_be_none(self): | |
| """ | |
| The value validator can be None. | |
| """ | |
| v = deep_mapping(key_validator=instance_of(str)) | |
| a = simple_attr("test") | |
| 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: | |
| """ | |
| Tests for `is_callable`. | |
| """ | |
| def test_in_all(self): | |
| """ | |
| Verify that this validator is in ``__all__``. | |
| """ | |
| assert is_callable.__name__ in validator_module.__all__ | |
| def test_success(self): | |
| """ | |
| If the value is callable, nothing happens. | |
| """ | |
| v = is_callable() | |
| a = simple_attr("test") | |
| v(None, a, isinstance) | |
| def test_fail(self): | |
| """ | |
| Raise TypeError if the value is not callable. | |
| """ | |
| v = is_callable() | |
| a = simple_attr("test") | |
| with pytest.raises(TypeError) as e: | |
| v(None, a, None) | |
| value = None | |
| message = "'test' must be callable (got {value} that is a {type_})." | |
| expected_message = message.format(value=value, type_=value.__class__) | |
| assert expected_message == e.value.args[0] | |
| assert value == e.value.args[1] | |
| assert expected_message == e.value.msg | |
| assert value == e.value.value | |
| def test_repr(self): | |
| """ | |
| Returned validator has a useful `__repr__`. | |
| """ | |
| v = is_callable() | |
| assert "<is_callable validator>" == repr(v) | |
| def test_exception_repr(self): | |
| """ | |
| Verify that NotCallableError exception has a useful `__str__`. | |
| """ | |
| from attr.exceptions import NotCallableError | |
| instance = NotCallableError(msg="Some Message", value=42) | |
| assert "Some Message" == str(instance) | |
| def test_hashability(): | |
| """ | |
| Validator classes are hashable. | |
| """ | |
| for obj_name in dir(validator_module): | |
| obj = getattr(validator_module, obj_name) | |
| if not has(obj): | |
| continue | |
| hash_func = getattr(obj, "__hash__", None) | |
| assert hash_func is not None | |
| assert hash_func is not object.__hash__ | |
| class TestLtLeGeGtNe: | |
| """ | |
| Tests for `Lt, Le, Ge, Gt, Ne`. | |
| """ | |
| BOUND = 4 | |
| def test_in_all(self): | |
| """ | |
| validator is in ``__all__``. | |
| """ | |
| assert all( | |
| f.__name__ in validator_module.__all__ | |
| for f in [lt, le, ge, gt, ne] | |
| ) | |
| @pytest.mark.parametrize("v", [lt, le, ge, gt, ne]) | |
| def test_retrieve_bound(self, v): | |
| """ | |
| The configured bound for the comparison can be extracted from the | |
| Attribute. | |
| """ | |
| @attr.s | |
| class Tester: | |
| value = attr.ib(validator=v(self.BOUND)) | |
| assert fields(Tester).value.validator.bound == self.BOUND | |
| @pytest.mark.parametrize( | |
| ("v", "value"), | |
| [ | |
| (ne, 3), | |
| (lt, 3), | |
| (le, 3), | |
| (le, 4), | |
| (ge, 4), | |
| (ge, 5), | |
| (gt, 5), | |
| (ne, 5), | |
| ], | |
| ) | |
| def test_check_valid(self, v, value): | |
| """Silent if value {op} bound.""" | |
| @attr.s | |
| class Tester: | |
| value = attr.ib(validator=v(self.BOUND)) | |
| Tester(value) # shouldn't raise exceptions | |
| @pytest.mark.parametrize( | |
| ("v", "value"), | |
| [ | |
| (lt, 4), | |
| (le, 5), | |
| (ge, 3), | |
| (gt, 4), | |
| (ne, 4), | |
| ], | |
| ) | |
| def test_check_invalid(self, v, value): | |
| """Raise ValueError if value {op} bound.""" | |
| @attr.s | |
| class Tester: | |
| value = attr.ib(validator=v(self.BOUND)) | |
| with pytest.raises(ValueError): | |
| Tester(value) | |
| @pytest.mark.parametrize("v", [lt, le, ge, gt, ne]) | |
| def test_repr(self, v): | |
| """ | |
| __repr__ is meaningful. | |
| """ | |
| nv = v(23) | |
| assert repr(nv) == f"<Validator for x {nv.compare_op} {23}>" | |
| class TestMaxLen: | |
| """ | |
| Tests for `max_len`. | |
| """ | |
| MAX_LENGTH = 4 | |
| def test_in_all(self): | |
| """ | |
| validator is in ``__all__``. | |
| """ | |
| assert max_len.__name__ in validator_module.__all__ | |
| def test_retrieve_max_len(self): | |
| """ | |
| The configured max. length can be extracted from the Attribute | |
| """ | |
| @attr.s | |
| class Tester: | |
| value = attr.ib(validator=max_len(self.MAX_LENGTH)) | |
| assert fields(Tester).value.validator.max_length == self.MAX_LENGTH | |
| @pytest.mark.parametrize( | |
| "value", | |
| [ | |
| "", | |
| "foo", | |
| "spam", | |
| [], | |
| list(range(MAX_LENGTH)), | |
| {"spam": 3, "eggs": 4}, | |
| ], | |
| ) | |
| def test_check_valid(self, value): | |
| """ | |
| Silent if len(value) <= max_len. | |
| Values can be strings and other iterables. | |
| """ | |
| @attr.s | |
| class Tester: | |
| value = attr.ib(validator=max_len(self.MAX_LENGTH)) | |
| Tester(value) # shouldn't raise exceptions | |
| @pytest.mark.parametrize( | |
| "value", | |
| [ | |
| "bacon", | |
| list(range(6)), | |
| ], | |
| ) | |
| def test_check_invalid(self, value): | |
| """ | |
| Raise ValueError if len(value) > max_len. | |
| """ | |
| @attr.s | |
| class Tester: | |
| value = attr.ib(validator=max_len(self.MAX_LENGTH)) | |
| with pytest.raises(ValueError): | |
| Tester(value) | |
| def test_repr(self): | |
| """ | |
| __repr__ is meaningful. | |
| """ | |
| assert repr(max_len(23)) == "<max_len validator for 23>" | |
| class TestMinLen: | |
| """ | |
| Tests for `min_len`. | |
| """ | |
| MIN_LENGTH = 2 | |
| def test_in_all(self): | |
| """ | |
| validator is in ``__all__``. | |
| """ | |
| assert min_len.__name__ in validator_module.__all__ | |
| def test_retrieve_min_len(self): | |
| """ | |
| The configured min. length can be extracted from the Attribute | |
| """ | |
| @attr.s | |
| class Tester: | |
| value = attr.ib(validator=min_len(self.MIN_LENGTH)) | |
| assert fields(Tester).value.validator.min_length == self.MIN_LENGTH | |
| @pytest.mark.parametrize( | |
| "value", | |
| [ | |
| "foo", | |
| "spam", | |
| list(range(MIN_LENGTH)), | |
| {"spam": 3, "eggs": 4}, | |
| ], | |
| ) | |
| def test_check_valid(self, value): | |
| """ | |
| Silent if len(value) => min_len. | |
| Values can be strings and other iterables. | |
| """ | |
| @attr.s | |
| class Tester: | |
| value = attr.ib(validator=min_len(self.MIN_LENGTH)) | |
| Tester(value) # shouldn't raise exceptions | |
| @pytest.mark.parametrize( | |
| "value", | |
| [ | |
| "", | |
| list(range(1)), | |
| ], | |
| ) | |
| def test_check_invalid(self, value): | |
| """ | |
| Raise ValueError if len(value) < min_len. | |
| """ | |
| @attr.s | |
| class Tester: | |
| value = attr.ib(validator=min_len(self.MIN_LENGTH)) | |
| with pytest.raises(ValueError): | |
| Tester(value) | |
| def test_repr(self): | |
| """ | |
| __repr__ is meaningful. | |
| """ | |
| assert repr(min_len(23)) == "<min_len validator for 23>" | |
| class TestSubclassOf: | |
| """ | |
| Tests for `_subclass_of`. | |
| """ | |
| def test_success(self): | |
| """ | |
| Nothing happens if classes match. | |
| """ | |
| v = _subclass_of(int) | |
| v(None, simple_attr("test"), int) | |
| def test_subclass(self): | |
| """ | |
| Subclasses are accepted too. | |
| """ | |
| v = _subclass_of(int) | |
| # yep, bools are a subclass of int :( | |
| v(None, simple_attr("test"), bool) | |
| def test_fail(self): | |
| """ | |
| Raises `TypeError` on wrong types. | |
| """ | |
| v = _subclass_of(int) | |
| a = simple_attr("test") | |
| with pytest.raises(TypeError) as e: | |
| v(None, a, str) | |
| assert ( | |
| "'test' must be a subclass of <class 'int'> (got <class 'str'>).", | |
| a, | |
| int, | |
| str, | |
| ) == e.value.args | |
| def test_repr(self): | |
| """ | |
| Returned validator has a useful `__repr__`. | |
| """ | |
| v = _subclass_of(int) | |
| assert ("<subclass_of validator for type <class 'int'>>") == repr(v) | |
| class TestNot_: | |
| """ | |
| Tests for `not_`. | |
| """ | |
| DEFAULT_EXC_TYPES = (ValueError, TypeError) | |
| def test_not_all(self): | |
| """ | |
| The validator is in ``__all__``. | |
| """ | |
| assert not_.__name__ in validator_module.__all__ | |
| def test_repr(self): | |
| """ | |
| Returned validator has a useful `__repr__`. | |
| """ | |
| wrapped = in_([3, 4, 5]) | |
| v = not_(wrapped) | |
| assert ( | |
| f"<not_ validator wrapping {wrapped!r}, capturing {v.exc_types!r}>" | |
| ) == repr(v) | |
| def test_success_because_fails(self): | |
| """ | |
| If the wrapped validator fails, we're happy. | |
| """ | |
| def always_fails(inst, attr, value): | |
| raise ValueError("always fails") | |
| v = not_(always_fails) | |
| a = simple_attr("test") | |
| input_value = 3 | |
| v(1, a, input_value) | |
| def test_fails_because_success(self): | |
| """ | |
| If the wrapped validator doesn't fail, not_ should fail. | |
| """ | |
| def always_passes(inst, attr, value): | |
| pass | |
| v = not_(always_passes) | |
| a = simple_attr("test") | |
| input_value = 3 | |
| with pytest.raises(ValueError) as e: | |
| v(1, a, input_value) | |
| assert ( | |
| ( | |
| f"not_ validator child '{always_passes!r}' did not raise a captured error" | |
| ), | |
| a, | |
| always_passes, | |
| input_value, | |
| self.DEFAULT_EXC_TYPES, | |
| ) == e.value.args | |
| def test_composable_with_in_pass(self): | |
| """ | |
| Check something is ``not in`` something else. | |
| """ | |
| v = not_(in_("abc")) | |
| a = simple_attr("test") | |
| input_value = "d" | |
| v(None, a, input_value) | |
| def test_composable_with_in_fail(self): | |
| """ | |
| Check something is ``not in`` something else, but it is, so fail. | |
| """ | |
| wrapped = in_("abc") | |
| v = not_(wrapped) | |
| a = simple_attr("test") | |
| input_value = "b" | |
| with pytest.raises(ValueError) as e: | |
| v(None, a, input_value) | |
| assert ( | |
| ( | |
| "not_ validator child '{!r}' did not raise a captured error" | |
| ).format(in_("abc")), | |
| a, | |
| wrapped, | |
| input_value, | |
| self.DEFAULT_EXC_TYPES, | |
| ) == e.value.args | |
| def test_composable_with_matches_re_pass(self): | |
| """ | |
| Check something does not match a regex. | |
| """ | |
| v = not_(matches_re("[a-z]{3}")) | |
| a = simple_attr("test") | |
| input_value = "spam" | |
| v(None, a, input_value) | |
| def test_composable_with_matches_re_fail(self): | |
| """ | |
| Check something does not match a regex, but it does, so fail. | |
| """ | |
| wrapped = matches_re("[a-z]{3}") | |
| v = not_(wrapped) | |
| a = simple_attr("test") | |
| input_value = "egg" | |
| with pytest.raises(ValueError) as e: | |
| v(None, a, input_value) | |
| assert ( | |
| ( | |
| f"not_ validator child '{wrapped!r}' did not raise a captured error" | |
| ), | |
| a, | |
| wrapped, | |
| input_value, | |
| self.DEFAULT_EXC_TYPES, | |
| ) == e.value.args | |
| def test_composable_with_instance_of_pass(self): | |
| """ | |
| Check something is not a type. This validator raises a TypeError, | |
| rather than a ValueError like the others. | |
| """ | |
| v = not_(instance_of((int, float))) | |
| a = simple_attr("test") | |
| v(None, a, "spam") | |
| def test_composable_with_instance_of_fail(self): | |
| """ | |
| Check something is not a type, but it is, so fail. | |
| """ | |
| wrapped = instance_of((int, float)) | |
| v = not_(wrapped) | |
| a = simple_attr("test") | |
| input_value = 2.718281828 | |
| with pytest.raises(ValueError) as e: | |
| v(None, a, input_value) | |
| assert ( | |
| ( | |
| f"not_ validator child '{instance_of((int, float))!r}' did not raise a captured error" | |
| ), | |
| a, | |
| wrapped, | |
| input_value, | |
| self.DEFAULT_EXC_TYPES, | |
| ) == e.value.args | |
| def test_custom_capture_match(self): | |
| """ | |
| Match a custom exception provided to `not_` | |
| """ | |
| v = not_(in_("abc"), exc_types=ValueError) | |
| a = simple_attr("test") | |
| v(None, a, "d") | |
| def test_custom_capture_miss(self): | |
| """ | |
| If the exception doesn't match, the underlying raise comes through | |
| """ | |
| class MyError(Exception): | |
| """:(""" | |
| wrapped = in_("abc") | |
| v = not_(wrapped, exc_types=MyError) | |
| a = simple_attr("test") | |
| input_value = "d" | |
| with pytest.raises(ValueError) as e: | |
| v(None, a, input_value) | |
| # get the underlying exception to compare | |
| with pytest.raises(Exception) as e_from_wrapped: | |
| wrapped(None, a, input_value) | |
| assert e_from_wrapped.value.args == e.value.args | |
| def test_custom_msg(self): | |
| """ | |
| If provided, use the custom message in the raised error | |
| """ | |
| custom_msg = "custom message!" | |
| wrapped = in_("abc") | |
| v = not_(wrapped, msg=custom_msg) | |
| a = simple_attr("test") | |
| input_value = "a" | |
| with pytest.raises(ValueError) as e: | |
| v(None, a, input_value) | |
| assert ( | |
| custom_msg, | |
| a, | |
| wrapped, | |
| input_value, | |
| self.DEFAULT_EXC_TYPES, | |
| ) == e.value.args | |
| def test_bad_exception_args(self): | |
| """ | |
| Malformed exception arguments | |
| """ | |
| wrapped = in_("abc") | |
| with pytest.raises(TypeError) as e: | |
| not_(wrapped, exc_types=(str, int)) | |
| assert ( | |
| "'exc_types' must be a subclass of <class 'Exception'> " | |
| "(got <class 'str'>)." | |
| ) == e.value.args[0] | |
| class TestOr: | |
| def test_in_all(self): | |
| """ | |
| Verify that this validator is in ``__all__``. | |
| """ | |
| assert or_.__name__ in validator_module.__all__ | |
| def test_success(self): | |
| """ | |
| Succeeds if at least one of wrapped validators succeed. | |
| """ | |
| v = or_(instance_of(str), always_pass) | |
| v(None, simple_attr("test"), 42) | |
| def test_fail(self): | |
| """ | |
| Fails if all wrapped validators fail. | |
| """ | |
| v = or_(instance_of(str), always_fail) | |
| with pytest.raises(ValueError): | |
| v(None, simple_attr("test"), 42) | |
| def test_repr(self): | |
| """ | |
| Returned validator has a useful `__repr__`. | |
| """ | |
| v = or_(instance_of(int), instance_of(str)) | |
| assert ( | |
| "<or validator wrapping (<instance_of validator for type " | |
| "<class 'int'>>, <instance_of validator for type <class 'str'>>)>" | |
| ) == repr(v) |