Skip to content

Commit fde0008

Browse files
Rewrite closure cells in decorated methods
1 parent 3823062 commit fde0008

2 files changed

Lines changed: 63 additions & 14 deletions

File tree

src/attr/_make.py

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,44 @@ def _make_cached_property_getattr(cached_properties, original_getattr, cls):
556556
)["__getattr__"]
557557

558558

559+
def _closure_cells(item, seen):
560+
"""
561+
Yield closure cells for *item* and any wrapped functions it closes over.
562+
"""
563+
item_id = id(item)
564+
if item_id in seen:
565+
return
566+
seen.add(item_id)
567+
568+
if isinstance(item, (classmethod, staticmethod)):
569+
item = item.__func__
570+
elif isinstance(item, property):
571+
for accessor in (item.fget, item.fset, item.fdel):
572+
if accessor is not None:
573+
yield from _closure_cells(accessor, seen)
574+
return
575+
elif isinstance(item, cached_property):
576+
item = item.func
577+
elif isinstance(item, types.MethodType):
578+
item = item.__func__
579+
580+
closure = getattr(item, "__closure__", None)
581+
if closure:
582+
yield from closure
583+
584+
for cell in closure:
585+
try:
586+
cell_contents = cell.cell_contents
587+
except ValueError:
588+
continue
589+
590+
yield from _closure_cells(cell_contents, seen)
591+
592+
wrapped = getattr(item, "__wrapped__", None)
593+
if wrapped is not None:
594+
yield from _closure_cells(wrapped, seen)
595+
596+
559597
def _frozen_setattrs(self, name, value):
560598
"""
561599
Attached to frozen classes as __setattr__.
@@ -973,20 +1011,7 @@ def _create_slots_class(self):
9731011
for item in itertools.chain(
9741012
cls.__dict__.values(), additional_closure_functions_to_update
9751013
):
976-
if isinstance(item, (classmethod, staticmethod)):
977-
# Class- and staticmethods hide their functions inside.
978-
# These might need to be rewritten as well.
979-
closure_cells = getattr(item.__func__, "__closure__", None)
980-
elif isinstance(item, property):
981-
# Workaround for property `super()` shortcut (PY3-only).
982-
# There is no universal way for other descriptors.
983-
closure_cells = getattr(item.fget, "__closure__", None)
984-
else:
985-
closure_cells = getattr(item, "__closure__", None)
986-
987-
if not closure_cells: # Catch None or the empty list.
988-
continue
989-
for cell in closure_cells:
1014+
for cell in _closure_cells(item, set()):
9901015
try:
9911016
match = cell.cell_contents is self._cls
9921017
except ValueError: # noqa: PERF203

tests/test_slots.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,30 @@ def statmethod():
497497

498498
assert D.statmethod() is D
499499

500+
def test_decorated_method(self, slots):
501+
"""
502+
Slotted classes rewrite closure cells in decorated methods.
503+
"""
504+
505+
def decorated(method):
506+
def wrapped(self, *args, **kwargs):
507+
return method(self, *args, **kwargs)
508+
509+
return wrapped
510+
511+
@attr.s(slots=slots)
512+
class A:
513+
def method(self):
514+
return "A"
515+
516+
@attr.s(slots=slots)
517+
class B(A):
518+
@decorated
519+
def method(self):
520+
return super().method()
521+
522+
assert B().method() == "A"
523+
500524

501525
@pytest.mark.skipif(PYPY, reason="__slots__ only block weakref on CPython")
502526
def test_not_weakrefable():

0 commit comments

Comments
 (0)