From 5a7e024c029e7564fe84a8f37686d90ba48a719d Mon Sep 17 00:00:00 2001 From: Dream <2468001320@qq.com> Date: Thu, 3 Sep 2026 12:31:41 +0800 Subject: [PATCH] fix: rewrite __class__ closure cells of methods hidden in decorators When a slotted class is re-created by _ClassBuilder, closure cells that reference the old class (baked in by no-arg super() or __class__) are rewritten to point at the new class. Methods wrapped in decorators hide their function - and thus the __class__ cell baked into it - behind the wrapper's own closure, so those cells were never rewritten. This caused no-arg super() inside such wrapped methods to raise: TypeError: super(type, obj): obj is not an instance or subtype of type Collect functions referenced by other functions' closures as well, so their cells are rewritten too. Fixes #1038. --- src/attr/_make.py | 46 +++++++++++++++++++++++++++++++++++---------- tests/test_slots.py | 30 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index afbca4635..61fdd6893 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -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 ) + 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: diff --git a/tests/test_slots.py b/tests/test_slots.py index a74c32b03..002bf9451 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -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():