Skip to content

Commit 82e9b0b

Browse files
bpo-46382 dataclass(slots=True) now takes inherited slots into account (GH-31980)
Do not include any members in __slots__ that are already in a base class's __slots__.
1 parent 383a3be commit 82e9b0b

File tree

4 files changed

+77
-9
lines changed

4 files changed

+77
-9
lines changed

Doc/library/dataclasses.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,16 @@ Module contents
188188

189189
.. versionadded:: 3.10
190190

191+
.. versionchanged:: 3.11
192+
If a field name is already included in the ``__slots__``
193+
of a base class, it will not be included in the generated ``__slots__``
194+
to prevent `overriding them <https://docs.python.org/3/reference/datamodel.html#notes-on-using-slots>`_.
195+
Therefore, do not use ``__slots__`` to retrieve the field names of a
196+
dataclass. Use :func:`fields` instead.
197+
To be able to determine inherited slots,
198+
base class ``__slots__`` may be any iterable, but *not* an iterator.
199+
200+
191201
``field``\s may optionally specify a default value, using normal
192202
Python syntax::
193203

Lib/dataclasses.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import keyword
77
import builtins
88
import functools
9+
import itertools
910
import abc
1011
import _thread
1112
from types import FunctionType, GenericAlias
@@ -1122,6 +1123,20 @@ def _dataclass_setstate(self, state):
11221123
object.__setattr__(self, field.name, value)
11231124

11241125

1126+
def _get_slots(cls):
1127+
match cls.__dict__.get('__slots__'):
1128+
case None:
1129+
return
1130+
case str(slot):
1131+
yield slot
1132+
# Slots may be any iterable, but we cannot handle an iterator
1133+
# because it will already be (partially) consumed.
1134+
case iterable if not hasattr(iterable, '__next__'):
1135+
yield from iterable
1136+
case _:
1137+
raise TypeError(f"Slots of '{cls.__name__}' cannot be determined")
1138+
1139+
11251140
def _add_slots(cls, is_frozen):
11261141
# Need to create a new class, since we can't set __slots__
11271142
# after a class has been created.
@@ -1133,7 +1148,13 @@ def _add_slots(cls, is_frozen):
11331148
# Create a new dict for our new class.
11341149
cls_dict = dict(cls.__dict__)
11351150
field_names = tuple(f.name for f in fields(cls))
1136-
cls_dict['__slots__'] = field_names
1151+
# Make sure slots don't overlap with those in base classes.
1152+
inherited_slots = set(
1153+
itertools.chain.from_iterable(map(_get_slots, cls.__mro__[1:-1]))
1154+
)
1155+
cls_dict["__slots__"] = tuple(
1156+
itertools.filterfalse(inherited_slots.__contains__, field_names)
1157+
)
11371158
for field_name in field_names:
11381159
# Remove our attributes, if present. They'll still be
11391160
# available in _MARKER.

Lib/test/test_dataclasses.py

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2926,23 +2926,58 @@ class C:
29262926
x: int
29272927

29282928
def test_generated_slots_value(self):
2929-
@dataclass(slots=True)
2930-
class Base:
2931-
x: int
29322929

2933-
self.assertEqual(Base.__slots__, ('x',))
2930+
class Root:
2931+
__slots__ = {'x'}
2932+
2933+
class Root2(Root):
2934+
__slots__ = {'k': '...', 'j': ''}
2935+
2936+
class Root3(Root2):
2937+
__slots__ = ['h']
2938+
2939+
class Root4(Root3):
2940+
__slots__ = 'aa'
29342941

29352942
@dataclass(slots=True)
2936-
class Delivered(Base):
2943+
class Base(Root4):
29372944
y: int
2945+
j: str
2946+
h: str
2947+
2948+
self.assertEqual(Base.__slots__, ('y', ))
2949+
2950+
@dataclass(slots=True)
2951+
class Derived(Base):
2952+
aa: float
2953+
x: str
2954+
z: int
2955+
k: str
2956+
h: str
29382957

2939-
self.assertEqual(Delivered.__slots__, ('x', 'y'))
2958+
self.assertEqual(Derived.__slots__, ('z', ))
29402959

29412960
@dataclass
2942-
class AnotherDelivered(Base):
2961+
class AnotherDerived(Base):
29432962
z: int
29442963

2945-
self.assertTrue('__slots__' not in AnotherDelivered.__dict__)
2964+
self.assertNotIn('__slots__', AnotherDerived.__dict__)
2965+
2966+
def test_cant_inherit_from_iterator_slots(self):
2967+
2968+
class Root:
2969+
__slots__ = iter(['a'])
2970+
2971+
class Root2(Root):
2972+
__slots__ = ('b', )
2973+
2974+
with self.assertRaisesRegex(
2975+
TypeError,
2976+
"^Slots of 'Root' cannot be determined"
2977+
):
2978+
@dataclass(slots=True)
2979+
class C(Root2):
2980+
x: int
29462981

29472982
def test_returns_new_class(self):
29482983
class A:
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:func:`~dataclasses.dataclass` ``slots=True`` now correctly omits slots already
2+
defined in base classes. Patch by Arie Bovenberg.

0 commit comments

Comments
 (0)