mirror of
https://github.com/python/cpython.git
synced 2025-09-27 02:39:58 +00:00
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__.
This commit is contained in:
parent
383a3bec74
commit
82e9b0bb0a
4 changed files with 77 additions and 9 deletions
|
@ -188,6 +188,16 @@ Module contents
|
||||||
|
|
||||||
.. versionadded:: 3.10
|
.. 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
|
``field``\s may optionally specify a default value, using normal
|
||||||
Python syntax::
|
Python syntax::
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import inspect
|
||||||
import keyword
|
import keyword
|
||||||
import builtins
|
import builtins
|
||||||
import functools
|
import functools
|
||||||
|
import itertools
|
||||||
import abc
|
import abc
|
||||||
import _thread
|
import _thread
|
||||||
from types import FunctionType, GenericAlias
|
from types import FunctionType, GenericAlias
|
||||||
|
@ -1122,6 +1123,20 @@ def _dataclass_setstate(self, state):
|
||||||
object.__setattr__(self, field.name, value)
|
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):
|
def _add_slots(cls, is_frozen):
|
||||||
# Need to create a new class, since we can't set __slots__
|
# Need to create a new class, since we can't set __slots__
|
||||||
# after a class has been created.
|
# after a class has been created.
|
||||||
|
@ -1133,7 +1148,13 @@ def _add_slots(cls, is_frozen):
|
||||||
# Create a new dict for our new class.
|
# Create a new dict for our new class.
|
||||||
cls_dict = dict(cls.__dict__)
|
cls_dict = dict(cls.__dict__)
|
||||||
field_names = tuple(f.name for f in fields(cls))
|
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:
|
for field_name in field_names:
|
||||||
# Remove our attributes, if present. They'll still be
|
# Remove our attributes, if present. They'll still be
|
||||||
# available in _MARKER.
|
# available in _MARKER.
|
||||||
|
|
|
@ -2926,23 +2926,58 @@ class TestSlots(unittest.TestCase):
|
||||||
x: int
|
x: int
|
||||||
|
|
||||||
def test_generated_slots_value(self):
|
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)
|
@dataclass(slots=True)
|
||||||
class Delivered(Base):
|
class Base(Root4):
|
||||||
y: int
|
y: int
|
||||||
|
j: str
|
||||||
|
h: str
|
||||||
|
|
||||||
self.assertEqual(Delivered.__slots__, ('x', 'y'))
|
self.assertEqual(Base.__slots__, ('y', ))
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class Derived(Base):
|
||||||
|
aa: float
|
||||||
|
x: str
|
||||||
|
z: int
|
||||||
|
k: str
|
||||||
|
h: str
|
||||||
|
|
||||||
|
self.assertEqual(Derived.__slots__, ('z', ))
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AnotherDelivered(Base):
|
class AnotherDerived(Base):
|
||||||
z: int
|
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):
|
def test_returns_new_class(self):
|
||||||
class A:
|
class A:
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
:func:`~dataclasses.dataclass` ``slots=True`` now correctly omits slots already
|
||||||
|
defined in base classes. Patch by Arie Bovenberg.
|
Loading…
Add table
Add a link
Reference in a new issue