Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 5c6e3b7

Browse filesBrowse files
ericvsmithcarljm
andauthored
gh-90562: Support zero argument super with dataclasses when slots=True (gh-124455)
Co-authored-by: @wookie184 Co-authored-by: Carl Meyer <carl@oddbird.net>
1 parent b6471f4 commit 5c6e3b7
Copy full SHA for 5c6e3b7

File tree

4 files changed

+177
-16
lines changed
Filter options

4 files changed

+177
-16
lines changed

‎Doc/library/dataclasses.rst

Copy file name to clipboardExpand all lines: Doc/library/dataclasses.rst
-7Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,6 @@ Module contents
187187
If :attr:`!__slots__` is already defined in the class, then :exc:`TypeError`
188188
is raised.
189189

190-
.. warning::
191-
Calling no-arg :func:`super` in dataclasses using ``slots=True``
192-
will result in the following exception being raised:
193-
``TypeError: super(type, obj): obj must be an instance or subtype of type``.
194-
The two-arg :func:`super` is a valid workaround.
195-
See :gh:`90562` for full details.
196-
197190
.. warning::
198191
Passing parameters to a base class :meth:`~object.__init_subclass__`
199192
when using ``slots=True`` will result in a :exc:`TypeError`.

‎Lib/dataclasses.py

Copy file name to clipboardExpand all lines: Lib/dataclasses.py
+49-8Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,9 +1218,31 @@ def _get_slots(cls):
12181218
raise TypeError(f"Slots of '{cls.__name__}' cannot be determined")
12191219

12201220

1221+
def _update_func_cell_for__class__(f, oldcls, newcls):
1222+
# Returns True if we update a cell, else False.
1223+
if f is None:
1224+
# f will be None in the case of a property where not all of
1225+
# fget, fset, and fdel are used. Nothing to do in that case.
1226+
return False
1227+
try:
1228+
idx = f.__code__.co_freevars.index("__class__")
1229+
except ValueError:
1230+
# This function doesn't reference __class__, so nothing to do.
1231+
return False
1232+
# Fix the cell to point to the new class, if it's already pointing
1233+
# at the old class. I'm not convinced that the "is oldcls" test
1234+
# is needed, but other than performance can't hurt.
1235+
closure = f.__closure__[idx]
1236+
if closure.cell_contents is oldcls:
1237+
closure.cell_contents = newcls
1238+
return True
1239+
return False
1240+
1241+
12211242
def _add_slots(cls, is_frozen, weakref_slot):
1222-
# Need to create a new class, since we can't set __slots__
1223-
# after a class has been created.
1243+
# Need to create a new class, since we can't set __slots__ after a
1244+
# class has been created, and the @dataclass decorator is called
1245+
# after the class is created.
12241246

12251247
# Make sure __slots__ isn't already set.
12261248
if '__slots__' in cls.__dict__:
@@ -1259,18 +1281,37 @@ def _add_slots(cls, is_frozen, weakref_slot):
12591281

12601282
# And finally create the class.
12611283
qualname = getattr(cls, '__qualname__', None)
1262-
cls = type(cls)(cls.__name__, cls.__bases__, cls_dict)
1284+
newcls = type(cls)(cls.__name__, cls.__bases__, cls_dict)
12631285
if qualname is not None:
1264-
cls.__qualname__ = qualname
1286+
newcls.__qualname__ = qualname
12651287

12661288
if is_frozen:
12671289
# Need this for pickling frozen classes with slots.
12681290
if '__getstate__' not in cls_dict:
1269-
cls.__getstate__ = _dataclass_getstate
1291+
newcls.__getstate__ = _dataclass_getstate
12701292
if '__setstate__' not in cls_dict:
1271-
cls.__setstate__ = _dataclass_setstate
1272-
1273-
return cls
1293+
newcls.__setstate__ = _dataclass_setstate
1294+
1295+
# Fix up any closures which reference __class__. This is used to
1296+
# fix zero argument super so that it points to the correct class
1297+
# (the newly created one, which we're returning) and not the
1298+
# original class. We can break out of this loop as soon as we
1299+
# make an update, since all closures for a class will share a
1300+
# given cell.
1301+
for member in newcls.__dict__.values():
1302+
# If this is a wrapped function, unwrap it.
1303+
member = inspect.unwrap(member)
1304+
1305+
if isinstance(member, types.FunctionType):
1306+
if _update_func_cell_for__class__(member, cls, newcls):
1307+
break
1308+
elif isinstance(member, property):
1309+
if (_update_func_cell_for__class__(member.fget, cls, newcls)
1310+
or _update_func_cell_for__class__(member.fset, cls, newcls)
1311+
or _update_func_cell_for__class__(member.fdel, cls, newcls)):
1312+
break
1313+
1314+
return newcls
12741315

12751316

12761317
def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,

‎Lib/test/test_dataclasses/__init__.py

Copy file name to clipboardExpand all lines: Lib/test/test_dataclasses/__init__.py
+125-1Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional, Protocol, DefaultDict
1818
from typing import get_type_hints
1919
from collections import deque, OrderedDict, namedtuple, defaultdict
20-
from functools import total_ordering
20+
from functools import total_ordering, wraps
2121

2222
import typing # Needed for the string "typing.ClassVar[int]" to work as an annotation.
2323
import dataclasses # Needed for the string "dataclasses.InitVar[int]" to work as an annotation.
@@ -4869,5 +4869,129 @@ class A:
48694869
self.assertEqual(fs[0].name, 'x')
48704870

48714871

4872+
class TestZeroArgumentSuperWithSlots(unittest.TestCase):
4873+
def test_zero_argument_super(self):
4874+
@dataclass(slots=True)
4875+
class A:
4876+
def foo(self):
4877+
super()
4878+
4879+
A().foo()
4880+
4881+
def test_dunder_class_with_old_property(self):
4882+
@dataclass(slots=True)
4883+
class A:
4884+
def _get_foo(slf):
4885+
self.assertIs(__class__, type(slf))
4886+
self.assertIs(__class__, slf.__class__)
4887+
return __class__
4888+
4889+
def _set_foo(slf, value):
4890+
self.assertIs(__class__, type(slf))
4891+
self.assertIs(__class__, slf.__class__)
4892+
4893+
def _del_foo(slf):
4894+
self.assertIs(__class__, type(slf))
4895+
self.assertIs(__class__, slf.__class__)
4896+
4897+
foo = property(_get_foo, _set_foo, _del_foo)
4898+
4899+
a = A()
4900+
self.assertIs(a.foo, A)
4901+
a.foo = 4
4902+
del a.foo
4903+
4904+
def test_dunder_class_with_new_property(self):
4905+
@dataclass(slots=True)
4906+
class A:
4907+
@property
4908+
def foo(slf):
4909+
return slf.__class__
4910+
4911+
@foo.setter
4912+
def foo(slf, value):
4913+
self.assertIs(__class__, type(slf))
4914+
4915+
@foo.deleter
4916+
def foo(slf):
4917+
self.assertIs(__class__, type(slf))
4918+
4919+
a = A()
4920+
self.assertIs(a.foo, A)
4921+
a.foo = 4
4922+
del a.foo
4923+
4924+
# Test the parts of a property individually.
4925+
def test_slots_dunder_class_property_getter(self):
4926+
@dataclass(slots=True)
4927+
class A:
4928+
@property
4929+
def foo(slf):
4930+
return __class__
4931+
4932+
a = A()
4933+
self.assertIs(a.foo, A)
4934+
4935+
def test_slots_dunder_class_property_setter(self):
4936+
@dataclass(slots=True)
4937+
class A:
4938+
foo = property()
4939+
@foo.setter
4940+
def foo(slf, val):
4941+
self.assertIs(__class__, type(slf))
4942+
4943+
a = A()
4944+
a.foo = 4
4945+
4946+
def test_slots_dunder_class_property_deleter(self):
4947+
@dataclass(slots=True)
4948+
class A:
4949+
foo = property()
4950+
@foo.deleter
4951+
def foo(slf):
4952+
self.assertIs(__class__, type(slf))
4953+
4954+
a = A()
4955+
del a.foo
4956+
4957+
def test_wrapped(self):
4958+
def mydecorator(f):
4959+
@wraps(f)
4960+
def wrapper(*args, **kwargs):
4961+
return f(*args, **kwargs)
4962+
return wrapper
4963+
4964+
@dataclass(slots=True)
4965+
class A:
4966+
@mydecorator
4967+
def foo(self):
4968+
super()
4969+
4970+
A().foo()
4971+
4972+
def test_remembered_class(self):
4973+
# Apply the dataclass decorator manually (not when the class
4974+
# is created), so that we can keep a reference to the
4975+
# undecorated class.
4976+
class A:
4977+
def cls(self):
4978+
return __class__
4979+
4980+
self.assertIs(A().cls(), A)
4981+
4982+
B = dataclass(slots=True)(A)
4983+
self.assertIs(B().cls(), B)
4984+
4985+
# This is undesirable behavior, but is a function of how
4986+
# modifying __class__ in the closure works. I'm not sure this
4987+
# should be tested or not: I don't really want to guarantee
4988+
# this behavior, but I don't want to lose the point that this
4989+
# is how it works.
4990+
4991+
# The underlying class is "broken" by changing its __class__
4992+
# in A.foo() to B. This normally isn't a problem, because no
4993+
# one will be keeping a reference to the underlying class A.
4994+
self.assertIs(A().cls(), B)
4995+
48724996
if __name__ == '__main__':
48734997
unittest.main()
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Modify dataclasses to support zero-argument super() when ``slots=True`` is
2+
specified. This works by modifying all references to ``__class__`` to point
3+
to the newly created class.

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.