Skip to content

Commit c241991

Browse files
authored
bpo-42269: Add slots parameter to dataclass decorator (GH-24171)
Add slots parameter to dataclass decorator and make_dataclass function.
1 parent 558df90 commit c241991

File tree

5 files changed

+111
-11
lines changed

5 files changed

+111
-11
lines changed

Doc/library/dataclasses.rst

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ directly specified in the ``InventoryItem`` definition shown above.
4646
Module-level decorators, classes, and functions
4747
-----------------------------------------------
4848

49-
.. decorator:: dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False)
49+
.. decorator:: dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False)
5050

5151
This function is a :term:`decorator` that is used to add generated
5252
:term:`special method`\s to classes, as described below.
@@ -79,7 +79,7 @@ Module-level decorators, classes, and functions
7979
class C:
8080
...
8181

82-
@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False)
82+
@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False)
8383
class C:
8484
...
8585

@@ -173,6 +173,11 @@ Module-level decorators, classes, and functions
173173
glossary entry for details. Also see the ``dataclasses.KW_ONLY``
174174
section.
175175

176+
- ``slots``: If true (the default is ``False``), :attr:`__slots__` attribute
177+
will be generated and new class will be returned instead of the original one.
178+
If :attr:`__slots__` is already defined in the class, then :exc:`TypeError`
179+
is raised.
180+
176181
``field``\s may optionally specify a default value, using normal
177182
Python syntax::
178183

@@ -337,7 +342,7 @@ Module-level decorators, classes, and functions
337342

338343
Raises :exc:`TypeError` if ``instance`` is not a dataclass instance.
339344

340-
.. 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)
345+
.. 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)
341346

342347
Creates a new dataclass with name ``cls_name``, fields as defined
343348
in ``fields``, base classes as given in ``bases``, and initialized
@@ -346,8 +351,8 @@ Module-level decorators, classes, and functions
346351
or ``(name, type, Field)``. If just ``name`` is supplied,
347352
``typing.Any`` is used for ``type``. The values of ``init``,
348353
``repr``, ``eq``, ``order``, ``unsafe_hash``, ``frozen``,
349-
``match_args``, and ``kw_only`` have the same meaning as they do
350-
in :func:`dataclass`.
354+
``match_args``, ``kw_only``, and ``slots`` have the same meaning as
355+
they do in :func:`dataclass`.
351356

352357
This function is not strictly required, because any Python
353358
mechanism for creating a new class with ``__annotations__`` can

Doc/whatsnew/3.10.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,12 @@ The ``BUTTON5_*`` constants are now exposed in the :mod:`curses` module if
895895
they are provided by the underlying curses library.
896896
(Contributed by Zackery Spytz in :issue:`39273`.)
897897
898+
dataclasses
899+
-----------
900+
901+
Added ``slots`` parameter in :func:`dataclasses.dataclass` decorator.
902+
(Contributed by Yurii Karabas in :issue:`42269`)
903+
898904
.. _distutils-deprecated:
899905
900906
distutils

Lib/dataclasses.py

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -874,7 +874,7 @@ def _hash_exception(cls, fields, globals):
874874

875875

876876
def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
877-
match_args, kw_only):
877+
match_args, kw_only, slots):
878878
# Now that dicts retain insertion order, there's no reason to use
879879
# an ordered dict. I am leveraging that ordering here, because
880880
# 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,
10861086
_set_new_attribute(cls, '__match_args__',
10871087
tuple(f.name for f in std_init_fields))
10881088

1089+
if slots:
1090+
cls = _add_slots(cls)
1091+
10891092
abc.update_abstractmethods(cls)
10901093

10911094
return cls
10921095

10931096

1097+
def _add_slots(cls):
1098+
# Need to create a new class, since we can't set __slots__
1099+
# after a class has been created.
1100+
1101+
# Make sure __slots__ isn't already set.
1102+
if '__slots__' in cls.__dict__:
1103+
raise TypeError(f'{cls.__name__} already specifies __slots__')
1104+
1105+
# Create a new dict for our new class.
1106+
cls_dict = dict(cls.__dict__)
1107+
field_names = tuple(f.name for f in fields(cls))
1108+
cls_dict['__slots__'] = field_names
1109+
for field_name in field_names:
1110+
# Remove our attributes, if present. They'll still be
1111+
# available in _MARKER.
1112+
cls_dict.pop(field_name, None)
1113+
1114+
# Remove __dict__ itself.
1115+
cls_dict.pop('__dict__', None)
1116+
1117+
# And finally create the class.
1118+
qualname = getattr(cls, '__qualname__', None)
1119+
cls = type(cls)(cls.__name__, cls.__bases__, cls_dict)
1120+
if qualname is not None:
1121+
cls.__qualname__ = qualname
1122+
1123+
return cls
1124+
1125+
10941126
def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,
10951127
unsafe_hash=False, frozen=False, match_args=True,
1096-
kw_only=False):
1128+
kw_only=False, slots=False):
10971129
"""Returns the same class as was passed in, with dunder methods
10981130
added based on the fields defined in the class.
10991131
@@ -1105,12 +1137,13 @@ def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,
11051137
__hash__() method function is added. If frozen is true, fields may
11061138
not be assigned to after instance creation. If match_args is true,
11071139
the __match_args__ tuple is added. If kw_only is true, then by
1108-
default all fields are keyword-only.
1140+
default all fields are keyword-only. If slots is true, an
1141+
__slots__ attribute is added.
11091142
"""
11101143

11111144
def wrap(cls):
11121145
return _process_class(cls, init, repr, eq, order, unsafe_hash,
1113-
frozen, match_args, kw_only)
1146+
frozen, match_args, kw_only, slots)
11141147

11151148
# See if we're being called as @dataclass or @dataclass().
11161149
if cls is None:
@@ -1269,7 +1302,7 @@ def _astuple_inner(obj, tuple_factory):
12691302

12701303
def make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True,
12711304
repr=True, eq=True, order=False, unsafe_hash=False,
1272-
frozen=False, match_args=True):
1305+
frozen=False, match_args=True, slots=False):
12731306
"""Return a new dynamically created dataclass.
12741307
12751308
The dataclass name will be 'cls_name'. 'fields' is an iterable
@@ -1336,7 +1369,7 @@ def exec_body_callback(ns):
13361369
# Apply the normal decorator.
13371370
return dataclass(cls, init=init, repr=repr, eq=eq, order=order,
13381371
unsafe_hash=unsafe_hash, frozen=frozen,
1339-
match_args=match_args)
1372+
match_args=match_args, slots=slots)
13401373

13411374

13421375
def replace(obj, /, **changes):

Lib/test/test_dataclasses.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2781,6 +2781,59 @@ class Derived(Base):
27812781
# We can add a new field to the derived instance.
27822782
d.z = 10
27832783

2784+
def test_generated_slots(self):
2785+
@dataclass(slots=True)
2786+
class C:
2787+
x: int
2788+
y: int
2789+
2790+
c = C(1, 2)
2791+
self.assertEqual((c.x, c.y), (1, 2))
2792+
2793+
c.x = 3
2794+
c.y = 4
2795+
self.assertEqual((c.x, c.y), (3, 4))
2796+
2797+
with self.assertRaisesRegex(AttributeError, "'C' object has no attribute 'z'"):
2798+
c.z = 5
2799+
2800+
def test_add_slots_when_slots_exists(self):
2801+
with self.assertRaisesRegex(TypeError, '^C already specifies __slots__$'):
2802+
@dataclass(slots=True)
2803+
class C:
2804+
__slots__ = ('x',)
2805+
x: int
2806+
2807+
def test_generated_slots_value(self):
2808+
@dataclass(slots=True)
2809+
class Base:
2810+
x: int
2811+
2812+
self.assertEqual(Base.__slots__, ('x',))
2813+
2814+
@dataclass(slots=True)
2815+
class Delivered(Base):
2816+
y: int
2817+
2818+
self.assertEqual(Delivered.__slots__, ('x', 'y'))
2819+
2820+
@dataclass
2821+
class AnotherDelivered(Base):
2822+
z: int
2823+
2824+
self.assertTrue('__slots__' not in AnotherDelivered.__dict__)
2825+
2826+
def test_returns_new_class(self):
2827+
class A:
2828+
x: int
2829+
2830+
B = dataclass(A, slots=True)
2831+
self.assertIsNot(A, B)
2832+
2833+
self.assertFalse(hasattr(A, "__slots__"))
2834+
self.assertTrue(hasattr(B, "__slots__"))
2835+
2836+
27842837
class TestDescriptors(unittest.TestCase):
27852838
def test_set_name(self):
27862839
# See bpo-33141.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add ``slots`` parameter to ``dataclasses.dataclass`` decorator to
2+
automatically generate ``__slots__`` for class. Patch provided by Yurii
3+
Karabas.

0 commit comments

Comments
 (0)