Skip to content

Commit

Permalink
NG: convert on setattr by default
Browse files Browse the repository at this point in the history
Not doing that from the get-go was an oversight.

Fixes #835
  • Loading branch information
hynek committed Dec 13, 2021
1 parent 8ae0bd9 commit 55cda74
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 6 deletions.
4 changes: 4 additions & 0 deletions changelog.d/835.breaking.rst
@@ -0,0 +1,4 @@
When using ``@attr.define``, converters are now run by default when setting an attribute on an instance -- additionally to validators.
I.e. the new default is ``on_setattr=[attr.setters.convert, attr.setters.validate]``.

This is unfortunately a breaking change, but it was an oversight, impossible to raise a ``DeprecationWarning`` about, and it's better to fix it now while the APIs are very fresh with few users.
4 changes: 3 additions & 1 deletion docs/api.rst
Expand Up @@ -707,13 +707,15 @@ The most notable differences are:
- *auto_exc=True*
- *auto_detect=True*
- *eq=True*, but *order=False*
- Validators run when you set an attribute (*on_setattr=attr.setters.validate*).
- Converters and validators are run when you set an attribute (*on_setattr=[attr.setters.convert, attr.setters.validate*]).
- Some options that aren't relevant to Python 3 have been dropped.

Please note that these are *defaults* and you're free to override them, just like before.

Since the Python ecosystem has settled on the term ``field`` for defining attributes, we have also added `attr.field` as a substitute for `attr.ib`.

.. versionchanged:: 21.3.0 Converters are also run ``on_setattr``.

.. note::

`attr.s` and `attr.ib` (and their serious business cousins) aren't going anywhere.
Expand Down
14 changes: 9 additions & 5 deletions src/attr/_next_gen.py
Expand Up @@ -35,8 +35,10 @@ def define(
match_args=True,
):
r"""
The only behavioral differences are the handling of the *auto_attribs*
option:
Define an ``attrs`` class.
The behavioral differences to `attr.s` are the handling of the
*auto_attribs* option:
:param Optional[bool] auto_attribs: If set to `True` or `False`, it behaves
exactly like `attr.s`. If left `None`, `attr.s` will try to guess:
Expand All @@ -46,9 +48,11 @@ def define(
2. Otherwise it assumes *auto_attribs=False* and tries to collect
`attr.ib`\ s.
and that mutable classes (``frozen=False``) validate on ``__setattr__``.
and that mutable classes (``frozen=False``) convert and validate on
``__setattr__``.
.. versionadded:: 20.1.0
.. versionchanged:: 21.3.0 Converters are also run ``on_setattr``.
"""

def do_it(cls, auto_attribs):
Expand Down Expand Up @@ -86,9 +90,9 @@ def wrap(cls):

had_on_setattr = on_setattr not in (None, setters.NO_OP)

# By default, mutable classes validate on setattr.
# By default, mutable classes convert & validate on setattr.
if frozen is False and on_setattr is None:
on_setattr = setters.validate
on_setattr = [setters.convert, setters.validate]

# However, if we subclass a frozen class, we inherit the immutability
# and disable on_setattr.
Expand Down
25 changes: 25 additions & 0 deletions tests/test_next_gen.py
Expand Up @@ -308,3 +308,28 @@ class MyException(Exception):

assert "foo" == ei.value.x
assert ei.value.__cause__ is None

def test_converts_and_validates_by_default(self):
"""
If no on_setattr is set, assume setters.convert, setters.validate.
"""

@attr.define
class C:
x: int = attr.field(converter=int)

@x.validator
def _v(self, _, value):
if value < 10:
raise ValueError("must be >=10")

inst = C(10)

# Converts
inst.x = "11"

assert 11 == inst.x

# Validates
with pytest.raises(ValueError, match="must be >=10"):
inst.x = "9"

0 comments on commit 55cda74

Please sign in to comment.