diff options
-rw-r--r-- | Doc/library/dataclasses.rst | 15 | ||||
-rw-r--r-- | Doc/whatsnew/3.10.rst | 6 | ||||
-rw-r--r-- | Lib/dataclasses.py | 45 | ||||
-rw-r--r-- | Lib/test/test_dataclasses.py | 53 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Library/2021-01-08-22-32-13.bpo-42269.W5v8z4.rst | 3 |
5 files changed, 111 insertions, 11 deletions
diff --git a/Doc/library/dataclasses.rst b/Doc/library/dataclasses.rst index 0e8db50..dbbc5d6 100644 --- a/Doc/library/dataclasses.rst +++ b/Doc/library/dataclasses.rst @@ -46,7 +46,7 @@ directly specified in the ``InventoryItem`` definition shown above. Module-level decorators, classes, and functions ----------------------------------------------- -.. decorator:: dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False) +.. decorator:: dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False) This function is a :term:`decorator` that is used to add generated :term:`special method`\s to classes, as described below. @@ -79,7 +79,7 @@ Module-level decorators, classes, and functions class C: ... - @dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False) + @dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False) class C: ... @@ -173,6 +173,11 @@ Module-level decorators, classes, and functions glossary entry for details. Also see the ``dataclasses.KW_ONLY`` section. + - ``slots``: If true (the default is ``False``), :attr:`__slots__` attribute + will be generated and new class will be returned instead of the original one. + If :attr:`__slots__` is already defined in the class, then :exc:`TypeError` + is raised. + ``field``\s may optionally specify a default value, using normal Python syntax:: @@ -337,7 +342,7 @@ Module-level decorators, classes, and functions Raises :exc:`TypeError` if ``instance`` is not a dataclass instance. -.. function:: make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False) +.. function:: make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False) Creates a new dataclass with name ``cls_name``, fields as defined in ``fields``, base classes as given in ``bases``, and initialized @@ -346,8 +351,8 @@ Module-level decorators, classes, and functions or ``(name, type, Field)``. If just ``name`` is supplied, ``typing.Any`` is used for ``type``. The values of ``init``, ``repr``, ``eq``, ``order``, ``unsafe_hash``, ``frozen``, - ``match_args``, and ``kw_only`` have the same meaning as they do - in :func:`dataclass`. + ``match_args``, ``kw_only``, and ``slots`` have the same meaning as + they do in :func:`dataclass`. This function is not strictly required, because any Python mechanism for creating a new class with ``__annotations__`` can diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index 1a39066..9c8e296 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -895,6 +895,12 @@ The ``BUTTON5_*`` constants are now exposed in the :mod:`curses` module if they are provided by the underlying curses library. (Contributed by Zackery Spytz in :issue:`39273`.) +dataclasses +----------- + +Added ``slots`` parameter in :func:`dataclasses.dataclass` decorator. +(Contributed by Yurii Karabas in :issue:`42269`) + .. _distutils-deprecated: distutils diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 3de50cf..5e57163 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -874,7 +874,7 @@ _hash_action = {(False, False, False, False): None, def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, - match_args, kw_only): + match_args, kw_only, slots): # Now that dicts retain insertion order, there's no reason to use # an ordered dict. I am leveraging that ordering here, because # derived class fields overwrite base class fields, but the order @@ -1086,14 +1086,46 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, _set_new_attribute(cls, '__match_args__', tuple(f.name for f in std_init_fields)) + if slots: + cls = _add_slots(cls) + abc.update_abstractmethods(cls) return cls +def _add_slots(cls): + # Need to create a new class, since we can't set __slots__ + # after a class has been created. + + # Make sure __slots__ isn't already set. + if '__slots__' in cls.__dict__: + raise TypeError(f'{cls.__name__} already specifies __slots__') + + # 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 + for field_name in field_names: + # Remove our attributes, if present. They'll still be + # available in _MARKER. + cls_dict.pop(field_name, None) + + # Remove __dict__ itself. + cls_dict.pop('__dict__', None) + + # And finally create the class. + qualname = getattr(cls, '__qualname__', None) + cls = type(cls)(cls.__name__, cls.__bases__, cls_dict) + if qualname is not None: + cls.__qualname__ = qualname + + return cls + + def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, - kw_only=False): + kw_only=False, slots=False): """Returns the same class as was passed in, with dunder methods added based on the fields defined in the class. @@ -1105,12 +1137,13 @@ def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False, __hash__() method function is added. If frozen is true, fields may not be assigned to after instance creation. If match_args is true, the __match_args__ tuple is added. If kw_only is true, then by - default all fields are keyword-only. + default all fields are keyword-only. If slots is true, an + __slots__ attribute is added. """ def wrap(cls): return _process_class(cls, init, repr, eq, order, unsafe_hash, - frozen, match_args, kw_only) + frozen, match_args, kw_only, slots) # See if we're being called as @dataclass or @dataclass(). if cls is None: @@ -1269,7 +1302,7 @@ def _astuple_inner(obj, tuple_factory): def make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, - frozen=False, match_args=True): + frozen=False, match_args=True, slots=False): """Return a new dynamically created dataclass. The dataclass name will be 'cls_name'. 'fields' is an iterable @@ -1336,7 +1369,7 @@ def make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, # Apply the normal decorator. return dataclass(cls, init=init, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen, - match_args=match_args) + match_args=match_args, slots=slots) def replace(obj, /, **changes): diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index 670648a..2fa0ae0 100644 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -2781,6 +2781,59 @@ class TestSlots(unittest.TestCase): # We can add a new field to the derived instance. d.z = 10 + def test_generated_slots(self): + @dataclass(slots=True) + class C: + x: int + y: int + + c = C(1, 2) + self.assertEqual((c.x, c.y), (1, 2)) + + c.x = 3 + c.y = 4 + self.assertEqual((c.x, c.y), (3, 4)) + + with self.assertRaisesRegex(AttributeError, "'C' object has no attribute 'z'"): + c.z = 5 + + def test_add_slots_when_slots_exists(self): + with self.assertRaisesRegex(TypeError, '^C already specifies __slots__$'): + @dataclass(slots=True) + class C: + __slots__ = ('x',) + x: int + + def test_generated_slots_value(self): + @dataclass(slots=True) + class Base: + x: int + + self.assertEqual(Base.__slots__, ('x',)) + + @dataclass(slots=True) + class Delivered(Base): + y: int + + self.assertEqual(Delivered.__slots__, ('x', 'y')) + + @dataclass + class AnotherDelivered(Base): + z: int + + self.assertTrue('__slots__' not in AnotherDelivered.__dict__) + + def test_returns_new_class(self): + class A: + x: int + + B = dataclass(A, slots=True) + self.assertIsNot(A, B) + + self.assertFalse(hasattr(A, "__slots__")) + self.assertTrue(hasattr(B, "__slots__")) + + class TestDescriptors(unittest.TestCase): def test_set_name(self): # See bpo-33141. diff --git a/Misc/NEWS.d/next/Library/2021-01-08-22-32-13.bpo-42269.W5v8z4.rst b/Misc/NEWS.d/next/Library/2021-01-08-22-32-13.bpo-42269.W5v8z4.rst new file mode 100644 index 0000000..595f873 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-01-08-22-32-13.bpo-42269.W5v8z4.rst @@ -0,0 +1,3 @@ +Add ``slots`` parameter to ``dataclasses.dataclass`` decorator to +automatically generate ``__slots__`` for class. Patch provided by Yurii +Karabas. |