Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
7 changes: 0 additions & 7 deletions Doc/library/dataclasses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -187,13 +187,6 @@ Module contents
If :attr:`!__slots__` is already defined in the class, then :exc:`TypeError`
is raised.

.. warning::
Calling no-arg :func:`super` in dataclasses using ``slots=True``
will result in the following exception being raised:
``TypeError: super(type, obj): obj must be an instance or subtype of type``.
The two-arg :func:`super` is a valid workaround.
See :gh:`90562` for full details.

.. warning::
Passing parameters to a base class :meth:`~object.__init_subclass__`
when using ``slots=True`` will result in a :exc:`TypeError`.
Expand Down
50 changes: 42 additions & 8 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1221,9 +1221,28 @@ def _get_slots(cls):
raise TypeError(f"Slots of '{cls.__name__}' cannot be determined")


def _update_func_cell_for__class__(f, oldcls, newcls):
if f is None:
# f will be None in the case of a property where not all of
# fget, fset, and fdel are used. Nothing to do in that case.
return
try:
idx = f.__code__.co_freevars.index("__class__")
except ValueError:
# This function doesn't reference __class__, so nothing to do.
return
# Fix the cell to point to the new class, if it's already pointing
# at the old class. I'm not convinced that the "is oldcls" test
# is needed, but other than performance can't hurt.
closure = f.__closure__[idx]
if closure.cell_contents is oldcls:
closure.cell_contents = newcls


def _add_slots(cls, is_frozen, weakref_slot):
# Need to create a new class, since we can't set __slots__
# after a class has been created.
# Need to create a new class, since we can't set __slots__ after a
# class has been created, and the @dataclass decorator is called
# after the class is created.

# Make sure __slots__ isn't already set.
if '__slots__' in cls.__dict__:
Expand Down Expand Up @@ -1262,18 +1281,33 @@ def _add_slots(cls, is_frozen, weakref_slot):

# And finally create the class.
qualname = getattr(cls, '__qualname__', None)
cls = type(cls)(cls.__name__, cls.__bases__, cls_dict)
newcls = type(cls)(cls.__name__, cls.__bases__, cls_dict)
if qualname is not None:
cls.__qualname__ = qualname
newcls.__qualname__ = qualname

if is_frozen:
# Need this for pickling frozen classes with slots.
if '__getstate__' not in cls_dict:
cls.__getstate__ = _dataclass_getstate
newcls.__getstate__ = _dataclass_getstate
if '__setstate__' not in cls_dict:
cls.__setstate__ = _dataclass_setstate

return cls
newcls.__setstate__ = _dataclass_setstate

# Fix up any closures which reference __class__. This is used to
# fix zero argument super so that it points to the correct class
# (the newly created one, which we're returning) and not the
# original class.
for member in newcls.__dict__.values():
# If this is a wrapped function, unwrap it.
member = inspect.unwrap(member)

if isinstance(member, types.FunctionType):
_update_func_cell_for__class__(member, cls, newcls)
elif isinstance(member, property):
_update_func_cell_for__class__(member.fget, cls, newcls)
_update_func_cell_for__class__(member.fset, cls, newcls)
_update_func_cell_for__class__(member.fdel, cls, newcls)
Comment thread
gpshead marked this conversation as resolved.
Outdated

return newcls


def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,
Expand Down
122 changes: 121 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

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 @@ -4869,5 +4869,125 @@ class A:
self.assertEqual(fs[0].name, 'x')


class TestZeroArgumentSuperWithSlots(unittest.TestCase):
def test_zero_argument_super(self):
@dataclass(slots=True)
class A:
def foo(self):
super()

A().foo()

def test_zero_argument_super_with_old_property(self):
Comment thread
ericvsmith marked this conversation as resolved.
Outdated
@dataclass(slots=True)
class A:
def _get_foo(slf):
return slf.__class__

def _set_foo(slf, value):
self.assertIs(__class__, type(slf))

def _del_foo(slf):
self.assertIs(__class__, type(slf))
Comment thread
ericvsmith marked this conversation as resolved.

foo = property(_get_foo, _set_foo, _del_foo)

a = A()
self.assertIs(a.foo, A)
a.foo = 4
del a.foo

def test_zero_argument_super_with_new_property(self):
Comment thread
ericvsmith marked this conversation as resolved.
Outdated
@dataclass(slots=True)
class A:
@property
def foo(slf):
return slf.__class__

@foo.setter
def foo(slf, value):
self.assertIs(__class__, type(slf))

@foo.deleter
def foo(slf):
self.assertIs(__class__, type(slf))

a = A()
self.assertIs(a.foo, A)
a.foo = 4
del a.foo

# Test the parts of a property individually.
def test_slots_dunder_class_property_getter(self):
@dataclass(slots=True)
class A:
@property
def foo(slf):
return __class__

a = A()
self.assertIs(a.foo, A)

def test_slots_dunder_class_property_setter(self):
@dataclass(slots=True)
class A:
foo = property()
@foo.setter
def foo(slf, val):
self.assertIs(__class__, type(slf))

a = A()
a.foo = 4

def test_slots_dunder_class_property_deleter(self):
@dataclass(slots=True)
class A:
foo = property()
@foo.deleter
def foo(slf):
self.assertIs(__class__, type(slf))
Comment thread
carljm marked this conversation as resolved.

a = A()
del a.foo

def test_wrapped(self):
def mydecorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
return wrapper

@dataclass(slots=True)
class A:
@mydecorator
def foo(self):
super()

A().foo()

def test_remembered_class(self):
# Apply the dataclass decorator manually (not when the class
# is created), so that we can keep a reference to the
# undecorated class.
class A:
def cls(self):
return __class__

self.assertIs(A().cls(), A)

B = dataclass(slots=True)(A)
self.assertIs(B().cls(), B)

# This is probably undesirable behavior, but is a function of
Comment thread
gpshead marked this conversation as resolved.
Outdated
# how modifying __class__ in the closure works. I'm not sure
# this should be tested or not: I don't really want to
# guarantee this behavior, but I don't want to lose the point
# that this is how it works.

# The underlying class is "broken" by changing its __class__
# in A.foo() to B. This normally isn't a problem, because no
# one will be keeping a reference to the underlying class A.
Comment thread
carljm marked this conversation as resolved.
Outdated
self.assertIs(A().cls(), B)

if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Modify dataclasses to support zero-argument super() when ``slots=True`` is
specified. This works by modifying all references to ``__class__`` to point
to the newly created class.