[Python-checkins] bpo-46382 dataclass(slots=True) now takes inherited slots into account (GH-31980)

ericvsmith webhook-mailer at python.org
Sat Mar 19 17:01:24 EDT 2022


https://github.com/python/cpython/commit/82e9b0bb0ac44d4942b9e01b2cdd2ca85c17e563
commit: 82e9b0bb0ac44d4942b9e01b2cdd2ca85c17e563
branch: main
author: Arie Bovenberg <a.c.bovenberg at gmail.com>
committer: ericvsmith <ericvsmith at users.noreply.github.com>
date: 2022-03-19T17:01:17-04:00
summary:

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__.

files:
A Misc/NEWS.d/next/Library/2022-03-18-17-25-57.bpo-46382.zQUJ66.rst
M Doc/library/dataclasses.rst
M Lib/dataclasses.py
M Lib/test/test_dataclasses.py

diff --git a/Doc/library/dataclasses.rst b/Doc/library/dataclasses.rst
index 0f6985f0ba8c4..08568da19d71a 100644
--- a/Doc/library/dataclasses.rst
+++ b/Doc/library/dataclasses.rst
@@ -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::
 
diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py
index b327462080f99..6be7c7b5de917 100644
--- a/Lib/dataclasses.py
+++ b/Lib/dataclasses.py
@@ -6,6 +6,7 @@
 import keyword
 import builtins
 import functools
+import itertools
 import abc
 import _thread
 from types import FunctionType, GenericAlias
@@ -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.
@@ -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.
diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py
index 2f37ecdfca6b4..847bcd46a6926 100644
--- a/Lib/test/test_dataclasses.py
+++ b/Lib/test/test_dataclasses.py
@@ -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:
diff --git a/Misc/NEWS.d/next/Library/2022-03-18-17-25-57.bpo-46382.zQUJ66.rst b/Misc/NEWS.d/next/Library/2022-03-18-17-25-57.bpo-46382.zQUJ66.rst
new file mode 100644
index 0000000000000..9bec94969cb4c
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-03-18-17-25-57.bpo-46382.zQUJ66.rst
@@ -0,0 +1,2 @@
+:func:`~dataclasses.dataclass` ``slots=True`` now correctly omits slots already
+defined in base classes. Patch by Arie Bovenberg.



More information about the Python-checkins mailing list