Skip to content

Commit 289f77a

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

2 files changed

Lines changed: 96 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: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import functools
88
import pickle
9+
import types
910
import weakref
1011

1112
from unittest import mock
@@ -16,6 +17,7 @@
1617
import attrs
1718

1819
from attr._compat import PY_3_14_PLUS, PYPY
20+
from attr._make import _closure_cells
1921

2022

2123
# Pympler doesn't work on PyPy.
@@ -426,7 +428,38 @@ class C2(C1Bare):
426428
assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2)
427429

428430

431+
def _function_closing_over(value):
432+
def function():
433+
return value
434+
435+
return function
436+
437+
429438
class TestClosureCellRewriting:
439+
def test_closure_cells_handles_wrapped_callables(self):
440+
"""
441+
Closure cells are found inside wrapped function shapes.
442+
"""
443+
444+
marker = object()
445+
function = _function_closing_over(marker)
446+
wrapped_function = _function_closing_over(marker)
447+
448+
def wrapper():
449+
pass
450+
451+
wrapper.__wrapped__ = wrapped_function
452+
453+
for item in (
454+
functools.cached_property(function),
455+
types.MethodType(function, object()),
456+
wrapper,
457+
):
458+
assert any(
459+
cell.cell_contents is marker
460+
for cell in _closure_cells(item, set())
461+
)
462+
430463
def test_closure_cell_rewriting(self):
431464
"""
432465
Slotted classes support proper closure cell rewriting.
@@ -497,6 +530,30 @@ def statmethod():
497530

498531
assert D.statmethod() is D
499532

533+
def test_decorated_method(self, slots):
534+
"""
535+
Slotted classes rewrite closure cells in decorated methods.
536+
"""
537+
538+
def decorated(method):
539+
def wrapped(self, *args, **kwargs):
540+
return method(self, *args, **kwargs)
541+
542+
return wrapped
543+
544+
@attr.s(slots=slots)
545+
class A:
546+
def method(self):
547+
return "A"
548+
549+
@attr.s(slots=slots)
550+
class B(A):
551+
@decorated
552+
def method(self):
553+
return super().method()
554+
555+
assert B().method() == "A"
556+
500557

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

0 commit comments

Comments
 (0)