diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 85269eb37..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.31.1 + rev: v2.32.1 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 diff --git a/changelog.d/951.change.rst b/changelog.d/951.change.rst new file mode 100644 index 000000000..6ded5fe7b --- /dev/null +++ b/changelog.d/951.change.rst @@ -0,0 +1 @@ +``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 47fce5c16..ecaee7e68 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. @@ -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/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: diff --git a/docs/init.rst b/docs/init.rst index cbe2a1557..330642dab 100644 --- a/docs/init.rst +++ b/docs/init.rst @@ -147,6 +147,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 @@ -172,6 +174,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: diff --git a/src/attr/validators.py b/src/attr/validators.py index 6a1e198f8..eece517da 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): @@ -322,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 f3fe69cc1..51fe2f41e 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -469,9 +469,16 @@ 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)",) == 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 +489,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): """