Skip to content

Commit

Permalink
Differentiate between own (= attrs) and custom (= user) __setattrs__
Browse files Browse the repository at this point in the history
Signed-off-by: Hynek Schlawack <hs@ox.cx>
  • Loading branch information
hynek committed Sep 4, 2020
1 parent aea0a7b commit c5b690f
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 17 deletions.
35 changes: 18 additions & 17 deletions src/attr/_make.py
Expand Up @@ -556,6 +556,7 @@ class _ClassBuilder(object):
"_slots",
"_weakref_slot",
"_has_own_setattr",
"_has_custom_setattr",
)

def __init__(
Expand Down Expand Up @@ -593,7 +594,8 @@ def __init__(
self._is_exc = is_exc
self._on_setattr = on_setattr

self._has_own_setattr = has_custom_setattr
self._has_custom_setattr = has_custom_setattr
self._has_own_setattr = False

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

Expand Down Expand Up @@ -654,9 +656,11 @@ def _patch_original_class(self):
if not self._has_own_setattr and getattr(
cls, "__attrs_own_setattr__", False
):
cls.__setattr__ = object.__setattr__
cls.__attrs_own_setattr__ = False

if not self._has_custom_setattr:
cls.__setattr__ = object.__setattr__

return cls

def _create_slots_class(self):
Expand All @@ -679,13 +683,16 @@ def _create_slots_class(self):
# XXX: class. See `test_slotted_confused` for details. For now that's
# XXX: OK with us.
if not self._has_own_setattr:
# There's metaclass magic that may result in a baseclass without
# __bases__ which results in _us_ not having one. cf. #681
for base_cls in getattr(self._cls, "__bases__", ()):
if base_cls.__dict__.get("__attrs_own_setattr__", False):
cd["__setattr__"] = object.__setattr__
cd["__attrs_own_setattr__"] = False
break
cd["__attrs_own_setattr__"] = False

if not self._has_custom_setattr:
# There's metaclass magic that may result in a baseclassa
# without __bases__ which results in _us_ not having one. cf.
# #681
for base_cls in getattr(self._cls, "__bases__", ()):
if base_cls.__dict__.get("__attrs_own_setattr__", False):
cd["__setattr__"] = object.__setattr__
break

# Traverse the MRO to check for an existing __weakref__.
weakref_inherited = False
Expand Down Expand Up @@ -864,20 +871,14 @@ def add_setattr(self):
if not sa_attrs:
return self

if self._has_own_setattr:
if self._has_custom_setattr:
# We need to write a __setattr__ but there already is one!
raise ValueError(
"Can't combine custom __setattr__ with on_setattr hooks."
)

cls = self._cls

# docstring comes from _add_method_dunders
def __setattr__(self, name, val):
"""
Method generated by attrs for class %s.
""" % (
cls.__name__,
)
try:
a, hook = sa_attrs[name]
except KeyError:
Expand Down
46 changes: 46 additions & 0 deletions tests/test_setattr.py
Expand Up @@ -340,6 +340,7 @@ def __setattr__(self, name, val):

i = RemoveNeedForOurSetAttr(1)

assert not RemoveNeedForOurSetAttr.__attrs_own_setattr__
assert 2 == i.x

@pytest.mark.parametrize("slots", [True, False])
Expand Down Expand Up @@ -387,3 +388,48 @@ class HookAndCustomSetAttr(object):

def __setattr__(self, _, __):
pass

@pytest.mark.parametrize("a_slots", [True, False])
@pytest.mark.parametrize("b_slots", [True, False])
@pytest.mark.parametrize("c_slots", [True, False])
def test_setattr_inherited_do_not_reset_intermediate(
self, a_slots, b_slots, c_slots
):
"""
A user-provided intermediate __setattr__ is not reset to
object.__setattr__.
This only can work on Python 3+ with auto_detect activated, such that
attrs can know, that there is a user-provided __setattr__.
"""

@attr.s(slots=a_slots)
class A(object):
x = attr.ib(on_setattr=setters.frozen)

@attr.s(slots=b_slots, auto_detect=True)
class B(A):
x = attr.ib(on_setattr=setters.NO_OP)

def __setattr__(self, key, value):
raise SystemError

@attr.s(slots=c_slots)
class C(B):
pass

assert getattr(A, "__attrs_own_setattr__", False) is True
assert getattr(B, "__attrs_own_setattr__", False) is False
assert getattr(C, "__attrs_own_setattr__", False) is False

with pytest.raises(SystemError):
C(1).x = 3

def test_docstring(self):
"""
Generated __setattr__ has a useful docstring.
"""
assert (
"Method generated by attrs for class WithOnSetAttrHook."
== WithOnSetAttrHook.__setattr__.__doc__
)

0 comments on commit c5b690f

Please sign in to comment.