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
46 changes: 36 additions & 10 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -974,20 +974,46 @@ def _create_slots_class(self):
# compiler will bake a reference to the class in the method itself
# as `method.__closure__`. Since we replace the class with a
# clone, we rewrite these references so it keeps working.
for item in itertools.chain(
cls.__dict__.values(), additional_closure_functions_to_update
):
if isinstance(item, (classmethod, staticmethod)):
#
# Methods wrapped in decorators hide their function (and thus the
# `__class__` cell baked into it) behind the wrapper's own closure,
# so we collect functions referenced by other functions' closures
# as well. (issue <https://github.com/python-attrs/attrs/issues/1038>)
to_rewrite = []
seen = set()

def _collect_closure_functions(obj):
if isinstance(obj, (classmethod, staticmethod)):
# Class- and staticmethods hide their functions inside.
# These might need to be rewritten as well.
closure_cells = getattr(item.__func__, "__closure__", None)
elif isinstance(item, property):
_collect_closure_functions(obj.__func__)
return
if isinstance(obj, property):
# Workaround for property `super()` shortcut (PY3-only).
# There is no universal way for other descriptors.
closure_cells = getattr(item.fget, "__closure__", None)
else:
closure_cells = getattr(item, "__closure__", None)
for f in (obj.fget, obj.fset, obj.fdel):
if f is not None:
_collect_closure_functions(f)
return
if isinstance(obj, type) or not callable(obj) or id(obj) in seen:
return
seen.add(id(obj))
to_rewrite.append(obj)
for cell in getattr(obj, "__closure__", ()) or ():
try:
content = cell.cell_contents
except ValueError:
# ValueError: Cell is empty
continue
if callable(content) and not isinstance(content, type):
_collect_closure_functions(content)

for item in itertools.chain(
cls.__dict__.values(), additional_closure_functions_to_update
):
_collect_closure_functions(item)

for item in to_rewrite:
closure_cells = getattr(item, "__closure__", None)
if not closure_cells: # Catch None or the empty list.
continue
for cell in closure_cells:
Expand Down
30 changes: 30 additions & 0 deletions tests/test_slots.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,36 @@ def statmethod():

assert D.statmethod() is D

def test_decorated_method_no_arg_super(self, slots):
"""
Slotted classes support proper closure cell rewriting for methods
that are wrapped in a decorator and use the no-arg super().
(issue https://github.com/python-attrs/attrs/issues/1038)

The decorator wrapper hides the actual method (whose closure contains
the ``__class__`` cell) behind its own closure, so the cell rewriting
has to look through nested closures as well.
"""

def decorated(method):
def wrapped(self, *args, **kwargs):
return method(self, *args, **kwargs)

return wrapped

@attr.s(slots=slots)
class A:
def f(self):
return "A.f"

@attr.s(slots=slots)
class B(A):
@decorated
def f(self):
return super().f() + " + B.f"

assert B().f() == "A.f + B.f"


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