bpo-42269: Add slots parameter to dataclass decorator (GH-24171)

Add slots parameter to dataclass decorator and make_dataclass function.
This commit is contained in:
Yurii Karabas 2021-05-01 05:14:30 +03:00 committed by GitHub
parent 558df90109
commit c24199184b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 111 additions and 11 deletions

View File

@ -46,7 +46,7 @@ directly specified in the ``InventoryItem`` definition shown above.
Module-level decorators, classes, and functions 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 This function is a :term:`decorator` that is used to add generated
:term:`special method`\s to classes, as described below. :term:`special method`\s to classes, as described below.
@ -79,7 +79,7 @@ Module-level decorators, classes, and functions
class C: 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: class C:
... ...
@ -173,6 +173,11 @@ Module-level decorators, classes, and functions
glossary entry for details. Also see the ``dataclasses.KW_ONLY`` glossary entry for details. Also see the ``dataclasses.KW_ONLY``
section. 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 ``field``\s may optionally specify a default value, using normal
Python syntax:: Python syntax::
@ -337,7 +342,7 @@ Module-level decorators, classes, and functions
Raises :exc:`TypeError` if ``instance`` is not a dataclass instance. 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 Creates a new dataclass with name ``cls_name``, fields as defined
in ``fields``, base classes as given in ``bases``, and initialized 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, or ``(name, type, Field)``. If just ``name`` is supplied,
``typing.Any`` is used for ``type``. The values of ``init``, ``typing.Any`` is used for ``type``. The values of ``init``,
``repr``, ``eq``, ``order``, ``unsafe_hash``, ``frozen``, ``repr``, ``eq``, ``order``, ``unsafe_hash``, ``frozen``,
``match_args``, and ``kw_only`` have the same meaning as they do ``match_args``, ``kw_only``, and ``slots`` have the same meaning as
in :func:`dataclass`. they do in :func:`dataclass`.
This function is not strictly required, because any Python This function is not strictly required, because any Python
mechanism for creating a new class with ``__annotations__`` can mechanism for creating a new class with ``__annotations__`` can

View File

@ -895,6 +895,12 @@ The ``BUTTON5_*`` constants are now exposed in the :mod:`curses` module if
they are provided by the underlying curses library. they are provided by the underlying curses library.
(Contributed by Zackery Spytz in :issue:`39273`.) (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-deprecated:
distutils distutils

View File

@ -874,7 +874,7 @@ _hash_action = {(False, False, False, False): None,
def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, 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 # Now that dicts retain insertion order, there's no reason to use
# an ordered dict. I am leveraging that ordering here, because # an ordered dict. I am leveraging that ordering here, because
# derived class fields overwrite base class fields, but the order # 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__', _set_new_attribute(cls, '__match_args__',
tuple(f.name for f in std_init_fields)) tuple(f.name for f in std_init_fields))
if slots:
cls = _add_slots(cls)
abc.update_abstractmethods(cls) abc.update_abstractmethods(cls)
return 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, def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,
unsafe_hash=False, frozen=False, match_args=True, 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 """Returns the same class as was passed in, with dunder methods
added based on the fields defined in the class. 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 __hash__() method function is added. If frozen is true, fields may
not be assigned to after instance creation. If match_args is true, 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 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): def wrap(cls):
return _process_class(cls, init, repr, eq, order, unsafe_hash, 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(). # See if we're being called as @dataclass or @dataclass().
if cls is None: 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, def make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True,
repr=True, eq=True, order=False, unsafe_hash=False, 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. """Return a new dynamically created dataclass.
The dataclass name will be 'cls_name'. 'fields' is an iterable 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. # Apply the normal decorator.
return dataclass(cls, init=init, repr=repr, eq=eq, order=order, return dataclass(cls, init=init, repr=repr, eq=eq, order=order,
unsafe_hash=unsafe_hash, frozen=frozen, unsafe_hash=unsafe_hash, frozen=frozen,
match_args=match_args) match_args=match_args, slots=slots)
def replace(obj, /, **changes): def replace(obj, /, **changes):

View File

@ -2781,6 +2781,59 @@ class TestSlots(unittest.TestCase):
# We can add a new field to the derived instance. # We can add a new field to the derived instance.
d.z = 10 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): class TestDescriptors(unittest.TestCase):
def test_set_name(self): def test_set_name(self):
# See bpo-33141. # See bpo-33141.

View File

@ -0,0 +1,3 @@
Add ``slots`` parameter to ``dataclasses.dataclass`` decorator to
automatically generate ``__slots__`` for class. Patch provided by Yurii
Karabas.