@@ -556,6 +556,63 @@ def _make_cached_property_getattr(cached_properties, original_getattr, cls):
556556 )["__getattr__" ]
557557
558558
559+ def _rewrite_closure_cells (item , old_cls , new_cls ):
560+ """
561+ Rewrite ``__class__`` / ``super()`` closure cells in *item* to point at
562+ *new_cls* instead of *old_cls*.
563+
564+ This is invoked on the class dict of the cloned slotted attrs class so
565+ that a method which references ``__class__`` or calls the no-arg
566+ ``super()`` keeps working — the compiler bakes a reference to the
567+ surrounding class into the method's ``__closure__``, and we just
568+ replaced that class with a clone.
569+
570+ A single pass through the class dict is not enough: a method may be
571+ wrapped by a decorator whose closure cell holds the *original* method,
572+ and the closure cell that actually points at the class lives on that
573+ inner method (see `GH-1038 <https://github.com/python-attrs/attrs/issues/1038>`_).
574+ We recurse into cell contents so decorator-wrapped methods get their
575+ inner ``__class__`` cell rewritten too.
576+
577+ Classmethod, staticmethod, and property descriptors are unwrapped
578+ before their ``__closure__`` is inspected. The recursion descends
579+ into any cell whose contents are themselves a function (the typical
580+ decorator-wraps-function case) but does not look inside arbitrary
581+ callables — we only want to rewrite closure cells the compiler put
582+ there as part of a ``super()`` / ``__class__`` reference, not user
583+ data that happens to live in a cell.
584+ """
585+ if isinstance (item , (classmethod , staticmethod )):
586+ # Class- and staticmethods hide their functions inside.
587+ # These might need to be rewritten as well.
588+ target = item .__func__
589+ elif isinstance (item , property ):
590+ # Workaround for property `super()` shortcut (PY3-only).
591+ # There is no universal way for other descriptors.
592+ target = item .fget
593+ else :
594+ target = item
595+
596+ closure_cells = getattr (target , "__closure__" , None )
597+ if not closure_cells : # Catch None or the empty list.
598+ return
599+
600+ for cell in closure_cells :
601+ try :
602+ contents = cell .cell_contents
603+ except ValueError : # noqa: PERF203
604+ # ValueError: Cell is empty
605+ continue
606+
607+ if contents is old_cls :
608+ cell .cell_contents = new_cls
609+ elif isinstance (contents , types .FunctionType ):
610+ # Decorator wrapping: the wrapper's cell points at the
611+ # underlying function, whose own closure may carry the
612+ # ``__class__`` / ``super()`` reference we need to rewrite.
613+ _rewrite_closure_cells (contents , old_cls , new_cls )
614+
615+
559616def _frozen_setattrs (self , name , value ):
560617 """
561618 Attached to frozen classes as __setattr__.
@@ -970,31 +1027,17 @@ def _create_slots_class(self):
9701027 # compiler will bake a reference to the class in the method itself
9711028 # as `method.__closure__`. Since we replace the class with a
9721029 # clone, we rewrite these references so it keeps working.
1030+ #
1031+ # The rewrite is recursive because a method may be wrapped by a
1032+ # decorator (decorator returns a wrapper function whose closure
1033+ # cell contains the original method; see GH-1038). Plain iteration
1034+ # over the class dict stops at the wrapper, so the inner method's
1035+ # baked `__class__` is left pointing at the old class and
1036+ # ``super()`` raises ``TypeError`` at call time.
9731037 for item in itertools .chain (
9741038 cls .__dict__ .values (), additional_closure_functions_to_update
9751039 ):
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 :
990- try :
991- match = cell .cell_contents is self ._cls
992- except ValueError : # noqa: PERF203
993- # ValueError: Cell is empty
994- pass
995- else :
996- if match :
997- cell .cell_contents = cls
1040+ _rewrite_closure_cells (item , self ._cls , cls )
9981041 return cls
9991042
10001043 def add_repr (self , ns ):
0 commit comments