Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5c6e3b7

Browse 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 <[email protected]>
1 parent b6471f4 commit 5c6e3b7

File tree

4 files changed

+177
-16
lines changed

4 files changed

+177
-16
lines changed

Doc/library/dataclasses.rst

Lines 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

Lines 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

Lines 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()
Lines 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)