diff --git a/changelog.d/1611.change.md b/changelog.d/1611.change.md new file mode 100644 index 000000000..8c1a31675 --- /dev/null +++ b/changelog.d/1611.change.md @@ -0,0 +1 @@ +Added a new equality validator `attrs.validators.eq()`. diff --git a/docs/api.rst b/docs/api.rst index c8ab29a41..16416a657 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -396,6 +396,22 @@ All objects from ``attrs.validators`` are also available from ``attr.validators` ... ValueError: ("'x' must be > 42: 42") +.. autofunction:: attrs.validators.eq + + For example: + + .. doctest:: + + >>> @define + ... class C: + ... x = field(validator=attrs.validators.eq(42)) + >>> C(42) + C(x=42) + >>> C(43) + Traceback (most recent call last): + ... + ValueError: ("'x' must be == 42: 41") + .. autofunction:: attrs.validators.ne For example: diff --git a/src/attr/validators.py b/src/attr/validators.py index d6fb2a47d..d66abd07e 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -21,6 +21,7 @@ "deep_iterable", "deep_mapping", "disabled", + "eq", "ge", "get_disabled", "gt", @@ -551,6 +552,21 @@ def ne(val): return _NumberValidator(val, "!=", operator.ne) +def eq(val): + """ + A validator that raises `ValueError` if the initializer is called with a + number not equal to *val*. + + The validator uses `operator.eq` to compare the values. + + Args: + val: The value that is allowed. + + .. versionadded:: 26.2.0 + """ + return _NumberValidator(val, "==", operator.eq) + + @attrs(repr=False, frozen=True, slots=True) class _MaxLengthValidator: max_length = attrib() diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index 384159216..2617bd72d 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -86,6 +86,7 @@ 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 eq(val: _T) -> _ValidatorType[_T]: ... def max_len(length: int) -> _ValidatorType[_T]: ... def min_len(length: int) -> _ValidatorType[_T]: ... def not_( diff --git a/tests/test_validators.py b/tests/test_validators.py index cbd087dd1..dafd147e4 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -18,6 +18,7 @@ and_, deep_iterable, deep_mapping, + eq, ge, gt, in_, @@ -878,7 +879,7 @@ def test_hashability(): class TestLtLeGeGtNe: """ - Tests for `Lt, Le, Ge, Gt, Ne`. + Tests for `Lt, Le, Ge, Gt, Ne, Eq`. """ BOUND = 4 @@ -889,10 +890,10 @@ def test_in_all(self): """ assert all( f.__name__ in validator_module.__all__ - for f in [lt, le, ge, gt, ne] + for f in [lt, le, ge, gt, ne, eq] ) - @pytest.mark.parametrize("v", [lt, le, ge, gt, ne]) + @pytest.mark.parametrize("v", [lt, le, ge, gt, ne, eq]) def test_retrieve_bound(self, v): """ The configured bound for the comparison can be extracted from the @@ -916,6 +917,7 @@ class Tester: (ge, 5), (gt, 5), (ne, 5), + (eq, 4), ], ) def test_check_valid(self, v, value): @@ -930,11 +932,13 @@ class Tester: @pytest.mark.parametrize( ("v", "value"), [ + (eq, 3), (lt, 4), (le, 5), (ge, 3), (gt, 4), (ne, 4), + (eq, 5), ], ) def test_check_invalid(self, v, value): @@ -947,7 +951,7 @@ class Tester: with pytest.raises(ValueError): Tester(value) - @pytest.mark.parametrize("v", [lt, le, ge, gt, ne]) + @pytest.mark.parametrize("v", [lt, le, ge, gt, ne, eq]) def test_repr(self, v): """ __repr__ is meaningful.