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.


``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__'):
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__'):
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

self.assertEqual(Base.__slots__, ('x',))
class Root:
__slots__ = {'x'}

class Root2(Root):
__slots__ = {'k': '...', 'j': ''}

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.