From bdd7ab20263bf1f610edc282c1e02a998631581d Mon Sep 17 00:00:00 2001 From: nativ Date: Sun, 30 Jan 2022 16:26:00 +0200 Subject: [PATCH 1/4] Added attrs.validators.min_len() --- changelog.d/foo.change.rst | 1 + docs/api.rst | 16 +++++++++ src/attr/validators.py | 32 +++++++++++++++++ src/attr/validators.pyi | 1 + tests/test_validators.py | 72 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 122 insertions(+) create mode 100644 changelog.d/foo.change.rst diff --git a/changelog.d/foo.change.rst b/changelog.d/foo.change.rst new file mode 100644 index 000000000..b093ea0c6 --- /dev/null +++ b/changelog.d/foo.change.rst @@ -0,0 +1 @@ +Added ``attrs.validators.min_len()``. \ No newline at end of file diff --git a/docs/api.rst b/docs/api.rst index 89c9de95d..e861fd9ee 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -524,6 +524,22 @@ All objects from ``attrs.validators`` are also available from ``attr.validators` ... ValueError: ("Length of 'x' must be <= 4: 5") +.. autofunction:: attrs.validators.min_len + + For example: + + .. doctest:: + + >>> @attrs.define + ... class C: + ... x = attrs.field(validator=attrs.validators.min_len(1)) + >>> C("bacon") + C(x='bacon') + >>> C("") + Traceback (most recent call last): + ... + ValueError: ("Length of 'x' must be => 1: 0") + .. autofunction:: attrs.validators.instance_of For example: diff --git a/src/attr/validators.py b/src/attr/validators.py index 0b0c8342f..b63dc1c77 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -37,6 +37,7 @@ "lt", "matches_re", "max_len", + "min_len", "optional", "provides", "set_disabled", @@ -559,3 +560,34 @@ def max_len(length): .. versionadded:: 21.3.0 """ return _MaxLengthValidator(length) + + +@attrs(repr=False, frozen=True, slots=True) +class _MinLengthValidator(object): + min_length = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if len(value) < self.min_length: + raise ValueError( + "Length of '{name}' must be => {min}: {len}".format( + name=attr.name, min=self.min_length, len=len(value) + ) + ) + + def __repr__(self): + return "".format(min=self.min_length) + + +def min_len(length): + """ + A validator that raises `ValueError` if the initializer is called + with a string or iterable that is shorter than *length*. + + :param int length: Minimum length of the string or iterable + + .. versionadded:: 21.5.0 + """ + return _MinLengthValidator(length) diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index 5e00b8543..81b9910f5 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -76,3 +76,4 @@ def le(val: _T) -> _ValidatorType[_T]: ... def ge(val: _T) -> _ValidatorType[_T]: ... def gt(val: _T) -> _ValidatorType[_T]: ... def max_len(length: int) -> _ValidatorType[_T]: ... +def min_len(length: int) -> _ValidatorType[_T]: ... diff --git a/tests/test_validators.py b/tests/test_validators.py index 38c39f348..69ec16456 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -28,6 +28,7 @@ lt, matches_re, max_len, + min_len, optional, provides, ) @@ -950,3 +951,74 @@ def test_repr(self): __repr__ is meaningful. """ assert repr(max_len(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(object): + 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(object): + 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(object): + 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)) == "" From b46a043048e69f26a6d15900b1d9f846ab96f7fb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 30 Jan 2022 14:51:41 +0000 Subject: [PATCH 2/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- changelog.d/foo.change.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/foo.change.rst b/changelog.d/foo.change.rst index b093ea0c6..4a5ccd22b 100644 --- a/changelog.d/foo.change.rst +++ b/changelog.d/foo.change.rst @@ -1 +1 @@ -Added ``attrs.validators.min_len()``. \ No newline at end of file +Added ``attrs.validators.min_len()``. From b7708cae5c9b081a5e42ad362ae0f1335d33df35 Mon Sep 17 00:00:00 2001 From: NI1993 <60190218+NI1993@users.noreply.github.com> Date: Sun, 30 Jan 2022 16:53:44 +0200 Subject: [PATCH 3/4] Rename foo.change.rst to 916.change.rst --- changelog.d/{foo.change.rst => 916.change.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{foo.change.rst => 916.change.rst} (100%) diff --git a/changelog.d/foo.change.rst b/changelog.d/916.change.rst similarity index 100% rename from changelog.d/foo.change.rst rename to changelog.d/916.change.rst From 927d67654468d0531d650bf19581ebaedb51503d Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Wed, 2 Feb 2022 10:02:10 +0100 Subject: [PATCH 4/4] Update src/attr/validators.py --- src/attr/validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/attr/validators.py b/src/attr/validators.py index b63dc1c77..7e3ff1635 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -588,6 +588,6 @@ def min_len(length): :param int length: Minimum length of the string or iterable - .. versionadded:: 21.5.0 + .. versionadded:: 22.1.0 """ return _MinLengthValidator(length)