Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -844,9 +844,14 @@ def _patch_original_class(self):

# If we've inherited an attrs __setattr__ and don't write our own,
# reset it to object's.
if not self._wrote_own_setattr and getattr(
cls, "__attrs_own_setattr__", False
):
has_attrs_own_setattr = False
for base_cls in cls.__mro__[1:]:
if "__setattr__" in base_cls.__dict__:
if base_cls.__dict__.get("__attrs_own_setattr__", False):
has_attrs_own_setattr = True
break

if not self._wrote_own_setattr and has_attrs_own_setattr:
cls.__attrs_own_setattr__ = False

if not self._has_custom_setattr:
Expand Down Expand Up @@ -880,9 +885,12 @@ def _create_slots_class(self):
cd["__attrs_own_setattr__"] = False

if not self._has_custom_setattr:
for base_cls in self._cls.__bases__:
if base_cls.__dict__.get("__attrs_own_setattr__", False):
cd["__setattr__"] = _OBJ_SETATTR
for base_cls in self._cls.__mro__[1:]:
if "__setattr__" in base_cls.__dict__:
if base_cls.__dict__.get(
"__attrs_own_setattr__", False
):
cd["__setattr__"] = _OBJ_SETATTR
break

# Traverse the MRO to collect existing slots
Expand Down
45 changes: 40 additions & 5 deletions tests/test_setattr.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,14 +316,10 @@ def __setattr__(self, key, value):
with pytest.raises(SystemError):
A().x = 1

@pytest.mark.xfail(raises=attr.exceptions.FrozenAttributeError)
def test_slotted_confused(self):
"""
If we have a in-between non-attrs class, setattr reset detection
should still work, but currently doesn't.

It works with dict classes because we can look the finished class and
patch it. With slotted classes we have to deduce it ourselves.
should still work.
"""

@attr.s(slots=True)
Expand All @@ -339,6 +335,45 @@ class C(B):

C(1).x = 2

def test_setattr_inherited_do_not_reset_intermediate_non_attrs(
self, slots
):
"""
A user-provided intermediate __setattr__ on a non-attrs class is not reset
to object.__setattr__.
"""

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

class BCustom(A):
x: int

def __setattr__(self, name, value):
object.__setattr__(self, name, value * 2)

class BPlain(A):
x: int

@attr.s(slots=slots)
class CFromCustom(BCustom):
x: int = attr.ib()

@attr.s(slots=slots)
class CFromPlain(BPlain):
x: int = attr.ib()

# CFromPlain should reset to object.__setattr__ and mutate normally
c_plain = CFromPlain(1)
c_plain.x = 2
assert c_plain.x == 2

# CFromCustom should respect and call BCustom.__setattr__
c_custom = CFromCustom(1)
c_custom.x = 2
assert c_custom.x == 4

def test_setattr_auto_detect_if_no_custom_setattr(self, slots):
"""
It's possible to remove the on_setattr hook from an attribute and
Expand Down