Skip to content

Commit 097a99f

Browse files
committed
Optimize the case of on_setattr=validate & no validators
This is important because define/mutable have on_setattr=setters.validate on default. Fixes #816
1 parent 8ae2d6f commit 097a99f

File tree

3 files changed

+69
-6
lines changed

3 files changed

+69
-6
lines changed

src/attr/_make.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -958,8 +958,7 @@ def add_init(self):
958958
self._cache_hash,
959959
self._base_attr_map,
960960
self._is_exc,
961-
self._on_setattr is not None
962-
and self._on_setattr is not setters.NO_OP,
961+
self._on_setattr,
963962
attrs_init=False,
964963
)
965964
)
@@ -978,8 +977,7 @@ def add_attrs_init(self):
978977
self._cache_hash,
979978
self._base_attr_map,
980979
self._is_exc,
981-
self._on_setattr is not None
982-
and self._on_setattr is not setters.NO_OP,
980+
self._on_setattr,
983981
attrs_init=True,
984982
)
985983
)
@@ -2008,13 +2006,19 @@ def _make_init(
20082006
cache_hash,
20092007
base_attr_map,
20102008
is_exc,
2011-
has_global_on_setattr,
2009+
global_on_setattr,
20122010
attrs_init,
20132011
):
2012+
has_global_on_setattr = (
2013+
global_on_setattr is not None
2014+
and global_on_setattr is not setters.NO_OP
2015+
)
2016+
20142017
if frozen and has_global_on_setattr:
20152018
raise ValueError("Frozen classes can't use on_setattr.")
20162019

20172020
needs_cached_setattr = cache_hash or frozen
2021+
has_validator = False
20182022
filtered_attrs = []
20192023
attr_dict = {}
20202024
for a in attrs:
@@ -2023,6 +2027,7 @@ def _make_init(
20232027

20242028
filtered_attrs.append(a)
20252029
attr_dict[a.name] = a
2030+
has_validator = has_validator or a.validator is not None
20262031

20272032
if a.on_setattr is not None:
20282033
if frozen is True:
@@ -2034,6 +2039,20 @@ def _make_init(
20342039
) or _is_slot_attr(a.name, base_attr_map):
20352040
needs_cached_setattr = True
20362041

2042+
# The default in define/mutable is a global on_setattr=setters.validate,
2043+
# however it's quite likely, that no attribute has actually a validator.
2044+
# Therefore, in the one case where on_setattr is validate and NO attribute
2045+
# has a validator defined, we pretend as if no class-level on_setattr is
2046+
# set.
2047+
if (
2048+
not frozen
2049+
and not has_validator
2050+
and global_on_setattr == setters.validate
2051+
):
2052+
needs_cached_setattr = False
2053+
has_global_on_setattr = False
2054+
global_on_setattr = setters.NO_OP
2055+
20372056
unique_filename = _generate_unique_filename(cls, "init")
20382057

20392058
script, globs, annotations = _attrs_to_init_script(

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+
global_on_setattr=None,
9898
attrs_init=False,
9999
)
100100
return cls

tests/test_functional.py

Lines changed: 44 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,46 @@ 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+
715+
def test_on_setattr_detect_inherited_validators(self):
716+
"""
717+
_make_init detects the presence of a validator even if the field is
718+
inherited.
719+
"""
720+
721+
@attr.s(on_setattr=attr.setters.validate)
722+
class C(object):
723+
x = attr.ib(validator=42)
724+
725+
@attr.s(on_setattr=attr.setters.validate)
726+
class D(C):
727+
y = attr.ib()
728+
729+
src = inspect.getsource(D.__init__)
730+
731+
assert "_setattr = _cached_setattr" in src
732+
assert "_setattr('x', x)" in src
733+
assert "_setattr('y', y)" in src

0 commit comments

Comments
 (0)