Skip to content
Open
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
1 change: 1 addition & 0 deletions changelog.d/1571.change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added new validator ``ne(val)`` (!= val).
16 changes: 16 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,22 @@ All objects from ``attrs.validators`` are also available from ``attr.validators`
...
ValueError: ("'x' must be > 42: 42")

.. autofunction:: attrs.validators.ne

For example:

.. doctest::

>>> @define
... class C:
... x = field(validator=attrs.validators.ne(42))
>>> C(43)
C(x=43)
>>> C(42)
Traceback (most recent call last):
...
ValueError: ("'x' must be != 42: 42")

.. autofunction:: attrs.validators.max_len

For example:
Expand Down
32 changes: 24 additions & 8 deletions src/attr/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"matches_re",
"max_len",
"min_len",
"ne",
"not_",
"optional",
"or_",
Expand Down Expand Up @@ -156,10 +157,10 @@ def matches_re(regex, flags=0, func=None):

Args:
regex (str, re.Pattern):
A regex string or precompiled pattern to match against
A regex string or precompiled pattern to match against.

flags (int):
Flags that will be passed to the underlying re function (default 0)
Flags that will be passed to the underlying re function (default 0).

func (typing.Callable):
Which underlying `re` function to call. Valid options are
Expand Down Expand Up @@ -368,8 +369,8 @@ def deep_iterable(member_validator, iterable_validator=None):
iterable_validator:
Validator(s) to apply to iterable itself (optional).

Raises
TypeError: if any sub-validators fail
Raises:
TypeError: if any sub-validators fail.

.. versionadded:: 19.1.0

Expand Down Expand Up @@ -513,7 +514,7 @@ def ge(val):
The validator uses `operator.ge` to compare the values.

Args:
val: Inclusive lower bound for values
val: Inclusive lower bound for values.

.. versionadded:: 21.3.0
"""
Expand All @@ -528,13 +529,28 @@ def gt(val):
The validator uses `operator.gt` to compare the values.

Args:
val: Exclusive lower bound for values
val: Exclusive lower bound for values.

.. versionadded:: 21.3.0
"""
return _NumberValidator(val, ">", operator.gt)


def ne(val):
"""
A validator that raises `ValueError` if the initializer is called with a
number equal to *val*.

The validator uses `operator.ne` to compare the values.

Args:
val: The value that is not allowed.

.. versionadded:: 26.2.0
"""
return _NumberValidator(val, "!=", operator.ne)


@attrs(repr=False, frozen=True, slots=True)
class _MaxLengthValidator:
max_length = attrib()
Expand All @@ -557,7 +573,7 @@ def max_len(length):
with a string or iterable that is longer than *length*.

Args:
length (int): Maximum length of the string or iterable
length (int): Maximum length of the string or iterable.

.. versionadded:: 21.3.0
"""
Expand Down Expand Up @@ -586,7 +602,7 @@ def min_len(length):
with a string or iterable that is shorter than *length*.

Args:
length (int): Minimum length of the string or iterable
length (int): Minimum length of the string or iterable.

.. versionadded:: 22.1.0
"""
Expand Down
1 change: 1 addition & 0 deletions src/attr/validators.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ def lt(val: _T) -> _ValidatorType[_T]: ...
def le(val: _T) -> _ValidatorType[_T]: ...
def ge(val: _T) -> _ValidatorType[_T]: ...
def gt(val: _T) -> _ValidatorType[_T]: ...
def ne(val: _T) -> _ValidatorType[_T]: ...
def max_len(length: int) -> _ValidatorType[_T]: ...
def min_len(length: int) -> _ValidatorType[_T]: ...
def not_(
Expand Down
15 changes: 10 additions & 5 deletions tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
matches_re,
max_len,
min_len,
ne,
not_,
optional,
or_,
Expand Down Expand Up @@ -875,9 +876,9 @@ def test_hashability():
assert hash_func is not object.__hash__


class TestLtLeGeGt:
class TestLtLeGeGtNe:
"""
Tests for `Lt, Le, Ge, Gt`.
Tests for `Lt, Le, Ge, Gt, Ne`.
"""

BOUND = 4
Expand All @@ -887,10 +888,11 @@ def test_in_all(self):
validator is in ``__all__``.
"""
assert all(
f.__name__ in validator_module.__all__ for f in [lt, le, ge, gt]
f.__name__ in validator_module.__all__
for f in [lt, le, ge, gt, ne]
)

@pytest.mark.parametrize("v", [lt, le, ge, gt])
@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
Expand All @@ -906,12 +908,14 @@ class Tester:
@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):
Expand All @@ -930,6 +934,7 @@ class Tester:
(le, 5),
(ge, 3),
(gt, 4),
(ne, 4),
],
)
def test_check_invalid(self, v, value):
Expand All @@ -942,7 +947,7 @@ class Tester:
with pytest.raises(ValueError):
Tester(value)

@pytest.mark.parametrize("v", [lt, le, ge, gt])
@pytest.mark.parametrize("v", [lt, le, ge, gt, ne])
def test_repr(self, v):
"""
__repr__ is meaningful.
Expand Down