Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-90562: Fix super() without args calls for dataclasses with slots #111538

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
30 changes: 30 additions & 0 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Comment on lines +1238 to +1239
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

classmethod and staticmethod have the __wrapped__ attribute since 3.10, so this code is perhaps dead.

elif isinstance(item, property):
closure_cells = getattr(item.fget, "__closure__", None)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if a getter doesn't have a closure, but a setter or deleter does?

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


Expand Down
155 changes: 154 additions & 1 deletion Lib/test/test_dataclasses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix ``super()`` call without arguments for :mod:`dataclasses` with
``slots=True``.
Loading