Skip to content

Commit 20c2d4f

Browse files
authored
Optimize the case of on_setattr=validate & no validators (#817)
* Optimize the case of on_setattr=validate & no validators This is important because define/mutable have on_setattr=setters.validate on default. Fixes #816 Signed-off-by: Hynek Schlawack <[email protected]> * Grammar
1 parent 8ae2d6f commit 20c2d4f

File tree

4 files changed

+74
-17
lines changed

4 files changed

+74
-17
lines changed

changelog.d/817.change.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
If the class-level *on_setattr* is set to ``attr.setters.validate`` (default in ``@attr.define`` and ``@attr.mutable``) but no field defines a validator, pretend that it's not set.

src/attr/_make.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -654,7 +654,7 @@ class _ClassBuilder(object):
654654
"_on_setattr",
655655
"_slots",
656656
"_weakref_slot",
657-
"_has_own_setattr",
657+
"_wrote_own_setattr",
658658
"_has_custom_setattr",
659659
)
660660

@@ -701,15 +701,23 @@ def __init__(
701701
self._on_setattr = on_setattr
702702

703703
self._has_custom_setattr = has_custom_setattr
704-
self._has_own_setattr = False
704+
self._wrote_own_setattr = False
705705

706706
self._cls_dict["__attrs_attrs__"] = self._attrs
707707

708708
if frozen:
709709
self._cls_dict["__setattr__"] = _frozen_setattrs
710710
self._cls_dict["__delattr__"] = _frozen_delattrs
711711

712-
self._has_own_setattr = True
712+
self._wrote_own_setattr = True
713+
elif on_setattr == setters.validate:
714+
for a in attrs:
715+
if a.validator is not None:
716+
break
717+
else:
718+
# If class-level on_setattr is set to validating, but there's
719+
# no field to validate, pretend like there's no on_setattr.
720+
self._on_setattr = None
713721

714722
if getstate_setstate:
715723
(
@@ -759,7 +767,7 @@ def _patch_original_class(self):
759767

760768
# If we've inherited an attrs __setattr__ and don't write our own,
761769
# reset it to object's.
762-
if not self._has_own_setattr and getattr(
770+
if not self._wrote_own_setattr and getattr(
763771
cls, "__attrs_own_setattr__", False
764772
):
765773
cls.__attrs_own_setattr__ = False
@@ -787,7 +795,7 @@ def _create_slots_class(self):
787795
# XXX: a non-attrs class and subclass the resulting class with an attrs
788796
# XXX: class. See `test_slotted_confused` for details. For now that's
789797
# XXX: OK with us.
790-
if not self._has_own_setattr:
798+
if not self._wrote_own_setattr:
791799
cd["__attrs_own_setattr__"] = False
792800

793801
if not self._has_custom_setattr:
@@ -958,8 +966,7 @@ def add_init(self):
958966
self._cache_hash,
959967
self._base_attr_map,
960968
self._is_exc,
961-
self._on_setattr is not None
962-
and self._on_setattr is not setters.NO_OP,
969+
self._on_setattr,
963970
attrs_init=False,
964971
)
965972
)
@@ -978,8 +985,7 @@ def add_attrs_init(self):
978985
self._cache_hash,
979986
self._base_attr_map,
980987
self._is_exc,
981-
self._on_setattr is not None
982-
and self._on_setattr is not setters.NO_OP,
988+
self._on_setattr,
983989
attrs_init=True,
984990
)
985991
)
@@ -1038,7 +1044,7 @@ def __setattr__(self, name, val):
10381044

10391045
self._cls_dict["__attrs_own_setattr__"] = True
10401046
self._cls_dict["__setattr__"] = self._add_method_dunders(__setattr__)
1041-
self._has_own_setattr = True
1047+
self._wrote_own_setattr = True
10421048

10431049
return self
10441050

@@ -2008,10 +2014,14 @@ def _make_init(
20082014
cache_hash,
20092015
base_attr_map,
20102016
is_exc,
2011-
has_global_on_setattr,
2017+
cls_on_setattr,
20122018
attrs_init,
20132019
):
2014-
if frozen and has_global_on_setattr:
2020+
has_cls_on_setattr = (
2021+
cls_on_setattr is not None and cls_on_setattr is not setters.NO_OP
2022+
)
2023+
2024+
if frozen and has_cls_on_setattr:
20152025
raise ValueError("Frozen classes can't use on_setattr.")
20162026

20172027
needs_cached_setattr = cache_hash or frozen
@@ -2030,7 +2040,7 @@ def _make_init(
20302040

20312041
needs_cached_setattr = True
20322042
elif (
2033-
has_global_on_setattr and a.on_setattr is not setters.NO_OP
2043+
has_cls_on_setattr and a.on_setattr is not setters.NO_OP
20342044
) or _is_slot_attr(a.name, base_attr_map):
20352045
needs_cached_setattr = True
20362046

@@ -2046,7 +2056,7 @@ def _make_init(
20462056
base_attr_map,
20472057
is_exc,
20482058
needs_cached_setattr,
2049-
has_global_on_setattr,
2059+
has_cls_on_setattr,
20502060
attrs_init,
20512061
)
20522062
if cls.__module__ in sys.modules:
@@ -2183,7 +2193,7 @@ def _attrs_to_init_script(
21832193
base_attr_map,
21842194
is_exc,
21852195
needs_cached_setattr,
2186-
has_global_on_setattr,
2196+
has_cls_on_setattr,
21872197
attrs_init,
21882198
):
21892199
"""
@@ -2257,7 +2267,7 @@ def fmt_setter_with_converter(
22572267

22582268
attr_name = a.name
22592269
has_on_setattr = a.on_setattr is not None or (
2260-
a.on_setattr is not setters.NO_OP and has_global_on_setattr
2270+
a.on_setattr is not setters.NO_OP and has_cls_on_setattr
22612271
)
22622272
arg_name = a.name.lstrip("_")
22632273

tests/test_dunders.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def _add_init(cls, frozen):
9494
cache_hash=False,
9595
base_attr_map={},
9696
is_exc=False,
97-
has_global_on_setattr=False,
97+
cls_on_setattr=None,
9898
attrs_init=False,
9999
)
100100
return cls

tests/test_functional.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from __future__ import absolute_import, division, print_function
66

7+
import inspect
78
import pickle
89

910
from copy import deepcopy
@@ -687,3 +688,48 @@ class C(object):
687688
"2021-06-01. Please use `eq` and `order` instead."
688689
== w.message.args[0]
689690
)
691+
692+
@pytest.mark.parametrize("slots", [True, False])
693+
def test_no_setattr_if_validate_without_validators(self, slots):
694+
"""
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__.
697+
698+
Regression test for #816.
699+
"""
700+
701+
@attr.s(on_setattr=attr.setters.validate)
702+
class C(object):
703+
x = attr.ib()
704+
705+
@attr.s(on_setattr=attr.setters.validate)
706+
class D(C):
707+
y = attr.ib()
708+
709+
src = inspect.getsource(D.__init__)
710+
711+
assert "setattr" not in src
712+
assert "self.x = x" in src
713+
assert "self.y = y" in src
714+
assert object.__setattr__ == D.__setattr__
715+
716+
def test_on_setattr_detect_inherited_validators(self):
717+
"""
718+
_make_init detects the presence of a validator even if the field is
719+
inherited.
720+
"""
721+
722+
@attr.s(on_setattr=attr.setters.validate)
723+
class C(object):
724+
x = attr.ib(validator=42)
725+
726+
@attr.s(on_setattr=attr.setters.validate)
727+
class D(C):
728+
y = attr.ib()
729+
730+
src = inspect.getsource(D.__init__)
731+
732+
assert "_setattr = _cached_setattr" in src
733+
assert "_setattr('x', x)" in src
734+
assert "_setattr('y', y)" in src
735+
assert object.__setattr__ != D.__setattr__

0 commit comments

Comments
 (0)