Skip to content
Merged
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
10 changes: 10 additions & 0 deletions Doc/library/dataclasses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,16 @@ Module contents

.. versionadded:: 3.10

.. versionchanged:: 3.11
If a field name is already included in the ``__slots__``
of a base class, it will not be included in the generated ``__slots__``
to prevent `overriding them <https://docs.python.org/3/reference/datamodel.html#notes-on-using-slots>`_.
Therefore, do not use ``__slots__`` to retrieve the field names of a
dataclass. Use :func:`fields` instead.
To be able to determine inherited slots,
base class ``__slots__`` may be any iterable, but *not* an iterator.

Comment thread
ericvsmith marked this conversation as resolved.

``field``\s may optionally specify a default value, using normal
Python syntax::

Expand Down
23 changes: 22 additions & 1 deletion Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import keyword
import builtins
import functools
import itertools
import abc
import _thread
from types import FunctionType, GenericAlias
Expand Down Expand Up @@ -1122,6 +1123,20 @@ def _dataclass_setstate(self, state):
object.__setattr__(self, field.name, value)


def _get_slots(cls):
match cls.__dict__.get('__slots__'):
Comment thread
ariebovenberg marked this conversation as resolved.
case None:
return
case str(slot):
yield slot
# Slots may be any iterable, but we cannot handle an iterator
# because it will already be (partially) consumed.
case iterable if not hasattr(iterable, '__next__'):
Comment thread
ariebovenberg marked this conversation as resolved.
yield from iterable
case _:
raise TypeError(f"Slots of '{cls.__name__}' cannot be determined")


def _add_slots(cls, is_frozen):
# Need to create a new class, since we can't set __slots__
# after a class has been created.
Expand All @@ -1133,7 +1148,13 @@ def _add_slots(cls, is_frozen):
# Create a new dict for our new class.
cls_dict = dict(cls.__dict__)
field_names = tuple(f.name for f in fields(cls))
cls_dict['__slots__'] = field_names
# Make sure slots don't overlap with those in base classes.
inherited_slots = set(
itertools.chain.from_iterable(map(_get_slots, cls.__mro__[1:-1]))
)
cls_dict["__slots__"] = tuple(
itertools.filterfalse(inherited_slots.__contains__, field_names)
)
for field_name in field_names:
# Remove our attributes, if present. They'll still be
# available in _MARKER.
Expand Down
51 changes: 43 additions & 8 deletions Lib/test/test_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2926,23 +2926,58 @@ class C:
x: int

def test_generated_slots_value(self):
@dataclass(slots=True)
class Base:
x: int

Comment thread
ariebovenberg marked this conversation as resolved.
self.assertEqual(Base.__slots__, ('x',))
class Root:
__slots__ = {'x'}

class Root2(Root):
__slots__ = {'k': '...', 'j': ''}
Comment thread
ariebovenberg marked this conversation as resolved.

class Root3(Root2):
__slots__ = ['h']

class Root4(Root3):
__slots__ = 'aa'

@dataclass(slots=True)
class Delivered(Base):
class Base(Root4):
y: int
j: str
h: str

self.assertEqual(Base.__slots__, ('y', ))

@dataclass(slots=True)
class Derived(Base):
aa: float
x: str
z: int
k: str
h: str

self.assertEqual(Delivered.__slots__, ('x', 'y'))
self.assertEqual(Derived.__slots__, ('z', ))

@dataclass
class AnotherDelivered(Base):
class AnotherDerived(Base):
z: int

self.assertTrue('__slots__' not in AnotherDelivered.__dict__)
self.assertNotIn('__slots__', AnotherDerived.__dict__)

def test_cant_inherit_from_iterator_slots(self):

class Root:
__slots__ = iter(['a'])

class Root2(Root):
__slots__ = ('b', )

with self.assertRaisesRegex(
TypeError,
"^Slots of 'Root' cannot be determined"
):
@dataclass(slots=True)
class C(Root2):
x: int

def test_returns_new_class(self):
class A:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:func:`~dataclasses.dataclass` ``slots=True`` now correctly omits slots already
defined in base classes. Patch by Arie Bovenberg.