Skip to content

Commit 679e4b4

Browse files
authored
NG: convert on setattr by default (#886)
* NG: convert on setattr by default Not doing that from the get-go was an oversight. Fixes #835 * Add optimization for default on_setattr w/ no work to do Otherwise we'd end up with an explicit setattr every time. * Fix optimization for NG default & j/ convert * NG is actually 3.6+ * Add test for convert optimization for good measure
1 parent 8ae0bd9 commit 679e4b4

File tree

7 files changed

+133
-17
lines changed

7 files changed

+133
-17
lines changed

changelog.d/835.breaking.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
When using ``@attr.define``, converters are now run by default when setting an attribute on an instance -- additionally to validators.
2+
I.e. the new default is ``on_setattr=[attr.setters.convert, attr.setters.validate]``.
3+
4+
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.

changelog.d/886.breaking.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
When using ``@attr.define``, converters are now run by default when setting an attribute on an instance -- additionally to validators.
2+
I.e. the new default is ``on_setattr=[attr.setters.convert, attr.setters.validate]``.
3+
4+
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.

docs/api.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -707,13 +707,15 @@ The most notable differences are:
707707
- *auto_exc=True*
708708
- *auto_detect=True*
709709
- *eq=True*, but *order=False*
710-
- Validators run when you set an attribute (*on_setattr=attr.setters.validate*).
710+
- Converters and validators are run when you set an attribute (*on_setattr=[attr.setters.convert, attr.setters.validate*]).
711711
- Some options that aren't relevant to Python 3 have been dropped.
712712

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

715715
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`.
716716

717+
.. versionchanged:: 21.3.0 Converters are also run ``on_setattr``.
718+
717719
.. note::
718720

719721
`attr.s` and `attr.ib` (and their serious business cousins) aren't going anywhere.

src/attr/_make.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@
5959
# Unique object for unequivocal getattr() defaults.
6060
_sentinel = object()
6161

62+
_ng_default_on_setattr = setters.pipe(setters.convert, setters.validate)
63+
6264

6365
class _Nothing(object):
6466
"""
@@ -722,13 +724,31 @@ def __init__(
722724
self._cls_dict["__delattr__"] = _frozen_delattrs
723725

724726
self._wrote_own_setattr = True
725-
elif on_setattr == setters.validate:
727+
elif on_setattr in (
728+
_ng_default_on_setattr,
729+
setters.validate,
730+
setters.convert,
731+
):
732+
has_validator = has_converter = False
726733
for a in attrs:
727734
if a.validator is not None:
735+
has_validator = True
736+
if a.converter is not None:
737+
has_converter = True
738+
739+
if has_validator and has_converter:
728740
break
729-
else:
730-
# If class-level on_setattr is set to validating, but there's
731-
# no field to validate, pretend like there's no on_setattr.
741+
if (
742+
(
743+
on_setattr == _ng_default_on_setattr
744+
and not (has_validator or has_converter)
745+
)
746+
or (on_setattr == setters.validate and not has_validator)
747+
or (on_setattr == setters.convert and not has_converter)
748+
):
749+
# If class-level on_setattr is set to convert + validate, but
750+
# there's no field to convert or validate, pretend like there's
751+
# no on_setattr.
732752
self._on_setattr = None
733753

734754
if getstate_setstate:
@@ -2123,9 +2143,7 @@ def _make_init(
21232143
raise ValueError("Frozen classes can't use on_setattr.")
21242144

21252145
needs_cached_setattr = True
2126-
elif (
2127-
has_cls_on_setattr and a.on_setattr is not setters.NO_OP
2128-
) or _is_slot_attr(a.name, base_attr_map):
2146+
elif has_cls_on_setattr and a.on_setattr is not setters.NO_OP:
21292147
needs_cached_setattr = True
21302148

21312149
unique_filename = _generate_unique_filename(cls, "init")

src/attr/_next_gen.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88
from attr.exceptions import UnannotatedAttributeError
99

1010
from . import setters
11-
from ._make import NOTHING, _frozen_setattrs, attrib, attrs
11+
from ._make import (
12+
NOTHING,
13+
_frozen_setattrs,
14+
_ng_default_on_setattr,
15+
attrib,
16+
attrs,
17+
)
1218

1319

1420
def define(
@@ -35,8 +41,10 @@ def define(
3541
match_args=True,
3642
):
3743
r"""
38-
The only behavioral differences are the handling of the *auto_attribs*
39-
option:
44+
Define an ``attrs`` class.
45+
46+
The behavioral differences to `attr.s` are the handling of the
47+
*auto_attribs* option:
4048
4149
:param Optional[bool] auto_attribs: If set to `True` or `False`, it behaves
4250
exactly like `attr.s`. If left `None`, `attr.s` will try to guess:
@@ -46,9 +54,11 @@ def define(
4654
2. Otherwise it assumes *auto_attribs=False* and tries to collect
4755
`attr.ib`\ s.
4856
49-
and that mutable classes (``frozen=False``) validate on ``__setattr__``.
57+
and that mutable classes (``frozen=False``) convert and validate on
58+
``__setattr__``.
5059
5160
.. versionadded:: 20.1.0
61+
.. versionchanged:: 21.3.0 Converters are also run ``on_setattr``.
5262
"""
5363

5464
def do_it(cls, auto_attribs):
@@ -86,9 +96,9 @@ def wrap(cls):
8696

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

89-
# By default, mutable classes validate on setattr.
99+
# By default, mutable classes convert & validate on setattr.
90100
if frozen is False and on_setattr is None:
91-
on_setattr = setters.validate
101+
on_setattr = _ng_default_on_setattr
92102

93103
# However, if we subclass a frozen class, we inherit the immutability
94104
# and disable on_setattr.

tests/test_functional.py

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import attr
1919

20-
from attr._compat import PY2, TYPE
20+
from attr._compat import PY2, PY36, TYPE
2121
from attr._make import NOTHING, Attribute
2222
from attr.exceptions import FrozenInstanceError
2323

@@ -692,8 +692,9 @@ class C(object):
692692
@pytest.mark.parametrize("slots", [True, False])
693693
def test_no_setattr_if_validate_without_validators(self, slots):
694694
"""
695-
If a class has on_setattr=attr.setters.validate (default in NG APIs)
696-
but sets no validators, don't use the (slower) setattr in __init__.
695+
If a class has on_setattr=attr.setters.validate (former default in NG
696+
APIs) but sets no validators, don't use the (slower) setattr in
697+
__init__.
697698
698699
Regression test for #816.
699700
"""
@@ -713,6 +714,58 @@ class D(C):
713714
assert "self.y = y" in src
714715
assert object.__setattr__ == D.__setattr__
715716

717+
@pytest.mark.parametrize("slots", [True, False])
718+
def test_no_setattr_if_convert_without_converters(self, slots):
719+
"""
720+
If a class has on_setattr=attr.setters.convert but sets no validators,
721+
don't use the (slower) setattr in __init__.
722+
"""
723+
724+
@attr.s(on_setattr=attr.setters.convert)
725+
class C(object):
726+
x = attr.ib()
727+
728+
@attr.s(on_setattr=attr.setters.convert)
729+
class D(C):
730+
y = attr.ib()
731+
732+
src = inspect.getsource(D.__init__)
733+
734+
assert "setattr" not in src
735+
assert "self.x = x" in src
736+
assert "self.y = y" in src
737+
assert object.__setattr__ == D.__setattr__
738+
739+
@pytest.mark.skipif(not PY36, reason="NG APIs are 3.6+")
740+
@pytest.mark.parametrize("slots", [True, False])
741+
def test_no_setattr_with_ng_defaults(self, slots):
742+
"""
743+
If a class has the NG default on_setattr=[convert, validate] but sets
744+
no validators or converters, don't use the (slower) setattr in
745+
__init__.
746+
"""
747+
748+
@attr.define
749+
class C(object):
750+
x = attr.ib()
751+
752+
src = inspect.getsource(C.__init__)
753+
754+
assert "setattr" not in src
755+
assert "self.x = x" in src
756+
assert object.__setattr__ == C.__setattr__
757+
758+
@attr.define
759+
class D(C):
760+
y = attr.ib()
761+
762+
src = inspect.getsource(D.__init__)
763+
764+
assert "setattr" not in src
765+
assert "self.x = x" in src
766+
assert "self.y = y" in src
767+
assert object.__setattr__ == D.__setattr__
768+
716769
def test_on_setattr_detect_inherited_validators(self):
717770
"""
718771
_make_init detects the presence of a validator even if the field is

tests/test_next_gen.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,28 @@ class MyException(Exception):
308308

309309
assert "foo" == ei.value.x
310310
assert ei.value.__cause__ is None
311+
312+
def test_converts_and_validates_by_default(self):
313+
"""
314+
If no on_setattr is set, assume setters.convert, setters.validate.
315+
"""
316+
317+
@attr.define
318+
class C:
319+
x: int = attr.field(converter=int)
320+
321+
@x.validator
322+
def _v(self, _, value):
323+
if value < 10:
324+
raise ValueError("must be >=10")
325+
326+
inst = C(10)
327+
328+
# Converts
329+
inst.x = "11"
330+
331+
assert 11 == inst.x
332+
333+
# Validates
334+
with pytest.raises(ValueError, match="must be >=10"):
335+
inst.x = "9"

0 commit comments

Comments
 (0)