Skip to content

Commit bad6a9c

Browse files
author
Rémi Lapeyre
committed
Add module and qualname arguments to dataclasses.make_dataclass()
1 parent 3f5fc70 commit bad6a9c

File tree

4 files changed

+94
-5
lines changed

4 files changed

+94
-5
lines changed

Doc/library/dataclasses.rst

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -322,16 +322,26 @@ Module-level decorators, classes, and functions
322322

323323
Raises :exc:`TypeError` if ``instance`` is not a dataclass instance.
324324

325-
.. function:: make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
325+
.. function:: make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, module=None, qualname=None)
326326

327327
Creates a new dataclass with name ``cls_name``, fields as defined
328328
in ``fields``, base classes as given in ``bases``, and initialized
329329
with a namespace as given in ``namespace``. ``fields`` is an
330330
iterable whose elements are each either ``name``, ``(name, type)``,
331331
or ``(name, type, Field)``. If just ``name`` is supplied,
332-
``typing.Any`` is used for ``type``. The values of ``init``,
333-
``repr``, ``eq``, ``order``, ``unsafe_hash``, and ``frozen`` have
334-
the same meaning as they do in :func:`dataclass`.
332+
``typing.Any`` is used for ``type``.
333+
334+
``module`` is the module in which the dataclass can be found, ``qualname`` is
335+
where in this module the dataclass can be found.
336+
337+
.. warning::
338+
339+
If ``module`` and ``qualname`` are not supplied and ``make_dataclass``
340+
cannot determine what they are, the new class will not be unpicklable;
341+
to keep errors close to the source, pickling will be disabled.
342+
343+
The values of ``init``, ``repr``, ``eq``, ``order``, ``unsafe_hash``, and
344+
``frozen`` have the same meaning as they do in :func:`dataclass`.
335345

336346
This function is not strictly required, because any Python
337347
mechanism for creating a new class with ``__annotations__`` can
@@ -356,6 +366,9 @@ Module-level decorators, classes, and functions
356366
def add_one(self):
357367
return self.x + 1
358368

369+
.. versionchanged:: 3.8
370+
The *module* and *qualname* parameters have been added.
371+
359372
.. function:: replace(instance, **changes)
360373

361374
Creates a new object of the same type of ``instance``, replacing

Lib/dataclasses.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import functools
99
import _thread
1010

11+
# Used by the functionnal API when the calling module is not known
12+
from enum import _make_class_unpicklable
1113

1214
__all__ = ['dataclass',
1315
'field',
@@ -1138,7 +1140,7 @@ def _astuple_inner(obj, tuple_factory):
11381140

11391141
def make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True,
11401142
repr=True, eq=True, order=False, unsafe_hash=False,
1141-
frozen=False):
1143+
frozen=False, module=None, qualname=None):
11421144
"""Return a new dynamically created dataclass.
11431145
11441146
The dataclass name will be 'cls_name'. 'fields' is an iterable
@@ -1158,6 +1160,14 @@ class C(Base):
11581160
11591161
For the bases and namespace parameters, see the builtin type() function.
11601162
1163+
'module' should be set to the module this class is being created in; if it
1164+
is not set, an attempt to find that module will be made, but if it fails the
1165+
class will not be picklable.
1166+
1167+
'qualname' should be set to the actual location this call can be found in
1168+
its module; by default it is set to the global scope. If this is not correct,
1169+
pickle will fail in some circumstances.
1170+
11611171
The parameters init, repr, eq, order, unsafe_hash, and frozen are passed to
11621172
dataclass().
11631173
"""
@@ -1198,6 +1208,22 @@ class C(Base):
11981208
# We use `types.new_class()` instead of simply `type()` to allow dynamic creation
11991209
# of generic dataclassses.
12001210
cls = types.new_class(cls_name, bases, {}, lambda ns: ns.update(namespace))
1211+
1212+
# TODO: this hack is the same that can be found in enum.py and should be
1213+
# removed if there ever is a way to get the caller module.
1214+
if module is None:
1215+
try:
1216+
module = sys._getframe(1).f_globals['__name__']
1217+
except (AttributeError, ValueError):
1218+
pass
1219+
if module is None:
1220+
_make_class_unpicklable(cls)
1221+
else:
1222+
cls.__module__ = module
1223+
1224+
if qualname is not None:
1225+
cls.__qualname__ = qualname
1226+
12011227
return dataclass(cls, init=init, repr=repr, eq=eq, order=order,
12021228
unsafe_hash=unsafe_hash, frozen=frozen)
12031229

Lib/test/test_dataclasses.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
import typing # Needed for the string "typing.ClassVar[int]" to work as an annotation.
1717
import dataclasses # Needed for the string "dataclasses.InitVar[int]" to work as an annotation.
1818

19+
# Used for pickle tests
20+
Scientist = make_dataclass('Scientist', [('name', str), ('level', str)])
21+
1922
# Just any custom exception we can catch.
2023
class CustomError(Exception): pass
2124

@@ -3047,6 +3050,50 @@ def test_funny_class_names_names(self):
30473050
C = make_dataclass(classname, ['a', 'b'])
30483051
self.assertEqual(C.__name__, classname)
30493052

3053+
def test_picklable(self):
3054+
d_knuth = Scientist(name='Donald Knuth', level='God')
3055+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
3056+
with self.subTest(proto=proto):
3057+
new_d_knuth = pickle.loads(pickle.dumps(d_knuth))
3058+
self.assertEqual(d_knuth, new_d_knuth)
3059+
self.assertIsNot(d_knuth, new_d_knuth)
3060+
3061+
def test_qualname(self):
3062+
d_knuth = Scientist(name='Donald Knuth', level='God')
3063+
self.assertEqual(d_knuth.__class__.__qualname__, 'Scientist')
3064+
3065+
ComputerScientist = make_dataclass(
3066+
'ComputerScientist',
3067+
[('name', str), ('level', str)],
3068+
qualname='Computer.Scientist'
3069+
)
3070+
d_knuth = ComputerScientist(name='Donald Knuth', level='God')
3071+
self.assertEqual(d_knuth.__class__.__qualname__, 'Computer.Scientist')
3072+
3073+
def test_module(self):
3074+
d_knuth = Scientist(name='Donald Knuth', level='God')
3075+
self.assertEqual(d_knuth.__module__, __name__)
3076+
3077+
ComputerScientist = make_dataclass(
3078+
'ComputerScientist',
3079+
[('name', str), ('level', str)],
3080+
module='other_module'
3081+
)
3082+
d_knuth = ComputerScientist(name='Donald Knuth', level='God')
3083+
self.assertEqual(d_knuth.__module__, 'other_module')
3084+
3085+
def test_unpicklable(self):
3086+
# if there is no way to determine the calling module, attempts to pickle
3087+
# an instance should raise TypeError
3088+
import sys
3089+
with unittest.mock.patch.object(sys, '_getframe', return_value=object()):
3090+
Scientist = make_dataclass('Scientist', [('name', str), ('level', str)])
3091+
3092+
d_knuth = Scientist(name='Donald Knuth', level='God')
3093+
self.assertEqual(d_knuth.__module__, '<unknown>')
3094+
with self.assertRaisesRegex(TypeError, 'cannot be pickled'):
3095+
pickle.dumps(d_knuth)
3096+
30503097
class TestReplace(unittest.TestCase):
30513098
def test(self):
30523099
@dataclass(frozen=True)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
`dataclasses.make_dataclass` accepts two new keyword arguments `module` and
2+
`qualname` in order to make created classes picklable. Patch contributed by
3+
Rémi Lapeyre.

0 commit comments

Comments
 (0)