diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 45ce5a98b51ae0..07b9e255b58fd6 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1212,6 +1212,7 @@ def _add_slots(cls, is_frozen, weakref_slot): # And finally create the class. qualname = getattr(cls, '__qualname__', None) + old_cls = cls cls = type(cls)(cls.__name__, cls.__bases__, cls_dict) if qualname is not None: cls.__qualname__ = qualname @@ -1223,6 +1224,35 @@ def _add_slots(cls, is_frozen, weakref_slot): if '__setstate__' not in cls_dict: cls.__setstate__ = _dataclass_setstate + # The following is a fix for + # https://github.com/python/cpython/issues/111500 + # The code is copied-and-modified from https://github.com/python-attrs/attrs + # All credits for it goes to the `attrs` team and contributors. + # + # If a method mentions `__class__` or uses the no-arg super(), the + # 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 cls.__dict__.values(): + item = inspect.unwrap(item) + if isinstance(item, (classmethod, staticmethod)): + closure_cells = getattr(item.__func__, "__closure__", None) + elif isinstance(item, property): + closure_cells = getattr(item.fget, "__closure__", None) + else: + closure_cells = getattr(item, "__closure__", None) + + if not closure_cells: + continue + for cell in closure_cells: + try: + match = cell.cell_contents is old_cls + except ValueError: # Cell is empty + pass + else: + if match: + cell.cell_contents = cls + return cls diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index ede74b0dd15ccf..ff22f60b81fa21 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -17,7 +17,7 @@ from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional, Protocol, DefaultDict from typing import get_type_hints from collections import deque, OrderedDict, namedtuple, defaultdict -from functools import total_ordering +from functools import total_ordering, wraps, cache import typing # Needed for the string "typing.ClassVar[int]" to work as an annotation. import dataclasses # Needed for the string "dataclasses.InitVar[int]" to work as an annotation. @@ -3497,6 +3497,159 @@ class A(Base): a_ref = weakref.ref(a) self.assertIs(a.__weakref__, a_ref) + def test_super_without_params_and_slots(self): + # https://github.com/python/cpython/issues/111500 + + def decorator(func): + @wraps(func) + def inner(*args, **kwargs): + return func(*args, **kwargs) + return inner + + for slots in (True, False): + with self.subTest(slots=slots): + @dataclass(slots=slots) + class Base: + x: int = 0 + def __post_init__(self): + self.x = 1 + def method(self): + return 2 + @decorator + def decorated(self): + return 3 + @property + def prop(self): + return 4 + @classmethod + def clsmethod(cls): + return 5 + @staticmethod + def stmethod(): + return 6 + + @dataclass(slots=slots) + class Child(Base): + y: int = 0 + z: int = 0 + def __post_init__(self): + self.y = 2 + super().__post_init__() + self.z = 3 + def method(self): + return super().method() + def decorated1(self): + return super().decorated() + @decorator + def decorated2(self): + return super().decorated() + @property + def prop(self): + return super().prop + @classmethod + def clsmethod(cls): + return super().clsmethod() + @staticmethod + def stmethod1(): + return super(Child, Child).stmethod() + @staticmethod + def stmethod2(): + return super().stmethod() + def cached1(self): + return super().cached() + + inst = Child() + self.assertEqual(inst.x, 1) + self.assertEqual(inst.y, 2) + self.assertEqual(inst.z, 3) + self.assertEqual(inst.method(), 2) + self.assertEqual(inst.decorated1(), 3) + self.assertEqual(inst.decorated2(), 3) + self.assertEqual(inst.prop, 4) + self.assertEqual(inst.clsmethod(), 5) + self.assertEqual(Child.clsmethod(), 5) + self.assertEqual(inst.stmethod1(), 6) + self.assertEqual(Child.stmethod1(), 6) + # These failures match regular classes: + msg = r"super\(\): no arguments" + with self.assertRaisesRegex(RuntimeError, msg): + inst.stmethod2() + with self.assertRaisesRegex(RuntimeError, msg): + Child.stmethod2() + + def test_super_without_params_wrapped(self): + for slots in (True, False): + with self.subTest(slots=slots): + @dataclass(slots=slots, frozen=True) + class Base: + @cache + def cached(self): + return 1 + def regular(self): + return 2 + @classmethod + @cache + def cached_cl(cls): + return 3 + @classmethod + def regular_cl(cls): + return 4 + + @dataclass(slots=slots, frozen=True) + class Child(Base): + def cached1(self): + return super().cached() + 10 + @cache + def cached2(self): + return super().cached() + 20 + @cache + def cached3(self): + return super().regular() + 30 + @classmethod + @cache + def cached_cl1(cls): + return super().cached_cl() + 40 + @classmethod + def cached_cl2(cls): + return super().cached_cl() + 50 + @classmethod + @cache + def cached_cl3(cls): + return super().regular_cl() + 60 + + inst = Child() + self.assertEqual(inst.cached(), 1) + self.assertEqual(inst.cached1(), 11) + self.assertEqual(inst.cached2(), 21) + self.assertEqual(inst.cached3(), 32) + self.assertEqual(inst.cached_cl(), 3) + self.assertEqual(inst.regular_cl(), 4) + self.assertEqual(inst.cached_cl1(), 43) + self.assertEqual(inst.cached_cl2(), 53) + self.assertEqual(inst.cached_cl3(), 64) + + def test_super_without_params_single_wrapper(self): + def decorator(func): + @wraps(func) + def inner(*args, **kwargs): + return func(*args, **kwargs) + return inner + + for slots in (True, False): + with self.subTest(slots=slots): + @dataclass(slots=True) + class A: + def method(self): + return 1 + + @dataclass(slots=True) + class B(A): + @decorator + def method(self): + return super().method() + + self.assertEqual(B().method(), 1) + class TestDescriptors(unittest.TestCase): def test_set_name(self): diff --git a/Misc/NEWS.d/next/Library/2023-10-31-14-19-50.gh-issue-111500.y914Ti.rst b/Misc/NEWS.d/next/Library/2023-10-31-14-19-50.gh-issue-111500.y914Ti.rst new file mode 100644 index 00000000000000..c88e3b296c61ad --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-10-31-14-19-50.gh-issue-111500.y914Ti.rst @@ -0,0 +1,2 @@ +Fix ``super()`` call without arguments for :mod:`dataclasses` with +``slots=True``.