blob: 9fd3f2dcbe6742f8434675632ffd2ccb7d37a2be [file] [log] [blame] [edit]
# SPDX-License-Identifier: MIT
"""
Tests for `attr.validators`.
"""
import re
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,
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
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)
assert (
"'func' must be one of None, fullmatch, match, search."
== 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 TestLtLeGeGt:
"""
Tests for `Lt, Le, Ge, Gt`.
"""
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]
)
@pytest.mark.parametrize("v", [lt, le, ge, gt])
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"),
[
(lt, 3),
(le, 3),
(le, 4),
(ge, 4),
(ge, 5),
(gt, 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),
],
)
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])
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)