From dbf2b3262b2716a9b56471e28e4f4785d556ae3d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 May 2022 14:40:38 -0600 Subject: [PATCH 1/7] [pre-commit.ci] pre-commit autoupdate (#955) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.31.1 → v2.32.0](https://github.com/asottile/pyupgrade/compare/v2.31.1...v2.32.0) - [github.com/pre-commit/pre-commit-hooks: v4.1.0 → v4.2.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.1.0...v4.2.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 85269eb37..26b14ac27 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - id: black - repo: https://github.com/asottile/pyupgrade - rev: v2.31.1 + rev: v2.32.0 hooks: - id: pyupgrade args: [--py3-plus, --keep-percent-format] @@ -37,7 +37,7 @@ repos: language_version: python3.10 # needed for match - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v4.2.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From f008d86a8e86b8af8ecdc3cdb9582a736ed27d54 Mon Sep 17 00:00:00 2001 From: Matthieu Melot Date: Fri, 6 May 2022 14:09:28 -0400 Subject: [PATCH 2/7] Typo fix (#957) Should read top-level, not to-level. --- docs/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index cd11e251b..74c482bc3 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -9,7 +9,7 @@ If you're confused by the many names, please check out `names` for clarification What follows is the API explanation, if you'd like a more hands-on introduction, have a look at `examples`. -As of version 21.3.0, ``attrs`` consists of **two** to-level package names: +As of version 21.3.0, ``attrs`` consists of **two** top-level package names: - The classic ``attr`` that powered the venerable `attr.s` and `attr.ib` - The modern ``attrs`` that only contains most modern APIs and relies on `attrs.define` and `attrs.field` to define your classes. From cbe9c9775e4158bd1b9f595413ef1e58841ec213 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Tue, 10 May 2022 07:25:11 +0200 Subject: [PATCH 3/7] Clarify validator arguments are passed as pos args Fixes #958 --- docs/init.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/init.rst b/docs/init.rst index d93683f1c..105fd1c93 100644 --- a/docs/init.rst +++ b/docs/init.rst @@ -146,6 +146,8 @@ The method has to accept three arguments: #. the *attribute* that it's validating, and finally #. the *value* that is passed for it. +These values are passed as *positional arguments*, therefore their names don't matter. + If the value does not pass the validator's standards, it just raises an appropriate exception. >>> @define @@ -171,6 +173,7 @@ Callables If you want to re-use your validators, you should have a look at the ``validator`` argument to `attrs.field`. It takes either a callable or a list of callables (usually functions) and treats them as validators that receive the same arguments as with the decorator approach. +Also as with the decorator approach, they are passed as *positional arguments* so you can name them however you want. Since the validators run *after* the instance is initialized, you can refer to other attributes while validating: From 4252adbab3aa05fe14b3112f31fbfc2edaa41e51 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Jun 2022 21:44:54 +0200 Subject: [PATCH 4/7] [pre-commit.ci] pre-commit autoupdate (#968) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 26b14ac27..9dd5e8e00 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - id: black - repo: https://github.com/asottile/pyupgrade - rev: v2.32.0 + rev: v2.32.1 hooks: - id: pyupgrade args: [--py3-plus, --keep-percent-format] From 985e8ef76716fe340520deab473ee65d2da70980 Mon Sep 17 00:00:00 2001 From: Dmitry Shmyrev <47178016+daemonix741@users.noreply.github.com> Date: Wed, 8 Jun 2022 14:38:09 +0300 Subject: [PATCH 5/7] Typo fix in examples (#966) * fixed typo * fixes according to comment Co-authored-by: Hynek Schlawack --- docs/examples.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/examples.rst b/docs/examples.rst index 1cf98ac86..ae5ffa78e 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -329,6 +329,8 @@ The method receives the partially initialized instance which enables you to base >>> C() C(x=1, y=2, z=[]) +Please keep in mind that the decorator approach *only* works if the attribute in question has a ``field`` assigned to it. +As a result, annotating an attribute with a type is *not* enough if you use ``@default``. .. _examples_validators: @@ -401,7 +403,7 @@ You can use a decorator: ValueError: value out of bounds Please note that the decorator approach only works if -- and only if! -- the attribute in question has a ``field`` assigned. -Therefore if you use ``@default``, it is *not* enough to annotate said attribute with a type. +Therefore if you use ``@validator``, it is *not* enough to annotate said attribute with a type. ``attrs`` ships with a bunch of validators, make sure to `check them out ` before writing your own: From 2257a0c60cba6a3cff0841c884981b5b7b99aef8 Mon Sep 17 00:00:00 2001 From: iron3oxide <98779754+iron3oxide@users.noreply.github.com> Date: Mon, 13 Jun 2022 07:57:47 +0200 Subject: [PATCH 6/7] Complete error signature of _InValidator (#951) The docstring of _in() says the ValueError would contain these, however it didn't. Adding said arguments enables uniform processing of ValueErrors raised by validators. One might for example want to extract the attr.name and the value that raised the error to show a custom, more user-friendly error message. --- changelog.d/951.change.rst | 3 +++ src/attr/validators.py | 5 ++++- tests/test_validators.py | 14 ++++++++++++-- 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 changelog.d/951.change.rst diff --git a/changelog.d/951.change.rst b/changelog.d/951.change.rst new file mode 100644 index 000000000..24303f6a9 --- /dev/null +++ b/changelog.d/951.change.rst @@ -0,0 +1,3 @@ +The error signature of _InValidator has been completed, +it now includes the attribute, +options and value as proclaimed in the docstring of _in(). diff --git a/src/attr/validators.py b/src/attr/validators.py index 6a1e198f8..3a4e72823 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -299,7 +299,10 @@ def __call__(self, inst, attr, value): raise ValueError( "'{name}' must be in {options!r} (got {value!r})".format( name=attr.name, options=self.options, value=value - ) + ), + attr, + self.options, + value, ) def __repr__(self): diff --git a/tests/test_validators.py b/tests/test_validators.py index f3fe69cc1..3bec62b03 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -471,7 +471,12 @@ def test_fail(self): a = simple_attr("test") with pytest.raises(ValueError) as e: v(None, a, None) - assert ("'test' must be in [1, 2, 3] (got None)",) == e.value.args + assert ( + "'test' must be in [1, 2, 3] (got None)", + a, + [1, 2, 3], + None, + ) == e.value.args def test_fail_with_string(self): """ @@ -482,7 +487,12 @@ def test_fail_with_string(self): a = simple_attr("test") with pytest.raises(ValueError) as e: v(None, a, None) - assert ("'test' must be in 'abc' (got None)",) == e.value.args + assert ( + "'test' must be in 'abc' (got None)", + a, + "abc", + None, + ) == e.value.args def test_repr(self): """ From a7e82b5c4121633cff792ed1cbc371843d590960 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 13 Jun 2022 08:33:23 +0200 Subject: [PATCH 7/7] Polish docs around #951 --- changelog.d/951.change.rst | 4 +--- docs/api.rst | 36 ++++++++++++++++++------------------ src/attr/validators.py | 4 ++++ tests/test_validators.py | 2 ++ 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/changelog.d/951.change.rst b/changelog.d/951.change.rst index 24303f6a9..6ded5fe7b 100644 --- a/changelog.d/951.change.rst +++ b/changelog.d/951.change.rst @@ -1,3 +1 @@ -The error signature of _InValidator has been completed, -it now includes the attribute, -options and value as proclaimed in the docstring of _in(). +``attrs.validators._in()``'s ``ValueError`` is not missing the attribute, expected options, and the value it got anymore. diff --git a/docs/api.rst b/docs/api.rst index 74c482bc3..37392ee49 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -566,24 +566,24 @@ All objects from ``attrs.validators`` are also available from ``attr.validators` .. doctest:: - >>> import enum - >>> class State(enum.Enum): - ... ON = "on" - ... OFF = "off" - >>> @attrs.define - ... class C: - ... state = attrs.field(validator=attrs.validators.in_(State)) - ... val = attrs.field(validator=attrs.validators.in_([1, 2, 3])) - >>> C(State.ON, 1) - C(state=, val=1) - >>> C("on", 1) - Traceback (most recent call last): - ... - ValueError: 'state' must be in (got 'on') - >>> C(State.ON, 4) - Traceback (most recent call last): - ... - ValueError: 'val' must be in [1, 2, 3] (got 4) + >>> import enum + >>> class State(enum.Enum): + ... ON = "on" + ... OFF = "off" + >>> @attrs.define + ... class C: + ... state = attrs.field(validator=attrs.validators.in_(State)) + ... val = attrs.field(validator=attrs.validators.in_([1, 2, 3])) + >>> C(State.ON, 1) + C(state=, val=1) + >>> C("on", 1) + Traceback (most recent call last): + ... + ValueError: 'state' must be in (got 'on'), Attribute(name='state', default=NOTHING, validator=>, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None), , 'on') + >>> C(State.ON, 4) + Traceback (most recent call last): + ... + ValueError: 'val' must be in [1, 2, 3] (got 4), Attribute(name='val', default=NOTHING, validator=, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None), [1, 2, 3], 4) .. autofunction:: attrs.validators.provides diff --git a/src/attr/validators.py b/src/attr/validators.py index 3a4e72823..eece517da 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -325,6 +325,10 @@ def in_(options): got. .. versionadded:: 17.1.0 + .. versionchanged:: 22.1.0 + The ValueError was incomplete until now and only contained the human + readable error message. Now it contains all the information that has + been promised since 17.1.0. """ return _InValidator(options) diff --git a/tests/test_validators.py b/tests/test_validators.py index 3bec62b03..51fe2f41e 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -469,8 +469,10 @@ def test_fail(self): """ 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,