diff --git a/Doc/library/dataclasses.rst b/Doc/library/dataclasses.rst index db5c3e0c7e2893..2d93dcf46c9697 100644 --- a/Doc/library/dataclasses.rst +++ b/Doc/library/dataclasses.rst @@ -322,16 +322,26 @@ 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) +.. 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) Creates a new dataclass with name ``cls_name``, fields as defined in ``fields``, base classes as given in ``bases``, and initialized with a namespace as given in ``namespace``. ``fields`` is an iterable whose elements are each either ``name``, ``(name, type)``, or ``(name, type, Field)``. If just ``name`` is supplied, - ``typing.Any`` is used for ``type``. The values of ``init``, - ``repr``, ``eq``, ``order``, ``unsafe_hash``, and ``frozen`` have - the same meaning as they do in :func:`dataclass`. + ``typing.Any`` is used for ``type``. + + ``module`` is the module in which the dataclass can be found, ``qualname`` is + where in this module the dataclass can be found. + + .. warning:: + + If ``module`` and ``qualname`` are not supplied and ``make_dataclass`` + cannot determine what they are, the new class will not be unpicklable; + to keep errors close to the source, pickling will be disabled. + + The values of ``init``, ``repr``, ``eq``, ``order``, ``unsafe_hash``, and + ``frozen`` 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 @@ -356,6 +366,9 @@ Module-level decorators, classes, and functions def add_one(self): return self.x + 1 + .. versionchanged:: 3.8 + The *module* and *qualname* parameters have been added. + .. function:: replace(instance, **changes) Creates a new object of the same type of ``instance``, replacing diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 71d9896a10524a..cbbfee9a617797 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -8,6 +8,8 @@ import functools import _thread +# Used by the functionnal API when the calling module is not known +from enum import _make_class_unpicklable __all__ = ['dataclass', 'field', @@ -1138,7 +1140,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): + frozen=False, module=None, qualname=None): """Return a new dynamically created dataclass. The dataclass name will be 'cls_name'. 'fields' is an iterable @@ -1158,6 +1160,14 @@ class C(Base): For the bases and namespace parameters, see the builtin type() function. + 'module' should be set to the module this class is being created in; if it + is not set, an attempt to find that module will be made, but if it fails the + class will not be picklable. + + 'qualname' should be set to the actual location this call can be found in + its module; by default it is set to the global scope. If this is not correct, + pickle will fail in some circumstances. + The parameters init, repr, eq, order, unsafe_hash, and frozen are passed to dataclass(). """ @@ -1198,6 +1208,22 @@ class C(Base): # We use `types.new_class()` instead of simply `type()` to allow dynamic creation # of generic dataclassses. cls = types.new_class(cls_name, bases, {}, lambda ns: ns.update(namespace)) + + # TODO: this hack is the same that can be found in enum.py and should be + # removed if there ever is a way to get the caller module. + if module is None: + try: + module = sys._getframe(1).f_globals['__name__'] + except (AttributeError, ValueError): + pass + if module is None: + _make_class_unpicklable(cls) + else: + cls.__module__ = module + + if qualname is not None: + cls.__qualname__ = qualname + return dataclass(cls, init=init, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen) diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index ff6060c6d2838a..bba86e934f804e 100755 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -16,6 +16,9 @@ import typing # Needed for the string "typing.ClassVar[int]" to work as an annotation. import dataclasses # Needed for the string "dataclasses.InitVar[int]" to work as an annotation. +# Used for pickle tests +Scientist = make_dataclass('Scientist', [('name', str), ('level', str)]) + # Just any custom exception we can catch. class CustomError(Exception): pass @@ -3047,6 +3050,50 @@ def test_funny_class_names_names(self): C = make_dataclass(classname, ['a', 'b']) self.assertEqual(C.__name__, classname) + def test_picklable(self): + d_knuth = Scientist(name='Donald Knuth', level='God') + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(proto=proto): + new_d_knuth = pickle.loads(pickle.dumps(d_knuth)) + self.assertEqual(d_knuth, new_d_knuth) + self.assertIsNot(d_knuth, new_d_knuth) + + def test_qualname(self): + d_knuth = Scientist(name='Donald Knuth', level='God') + self.assertEqual(d_knuth.__class__.__qualname__, 'Scientist') + + ComputerScientist = make_dataclass( + 'ComputerScientist', + [('name', str), ('level', str)], + qualname='Computer.Scientist' + ) + d_knuth = ComputerScientist(name='Donald Knuth', level='God') + self.assertEqual(d_knuth.__class__.__qualname__, 'Computer.Scientist') + + def test_module(self): + d_knuth = Scientist(name='Donald Knuth', level='God') + self.assertEqual(d_knuth.__module__, __name__) + + ComputerScientist = make_dataclass( + 'ComputerScientist', + [('name', str), ('level', str)], + module='other_module' + ) + d_knuth = ComputerScientist(name='Donald Knuth', level='God') + self.assertEqual(d_knuth.__module__, 'other_module') + + def test_unpicklable(self): + # if there is no way to determine the calling module, attempts to pickle + # an instance should raise TypeError + import sys + with unittest.mock.patch.object(sys, '_getframe', return_value=object()): + Scientist = make_dataclass('Scientist', [('name', str), ('level', str)]) + + d_knuth = Scientist(name='Donald Knuth', level='God') + self.assertEqual(d_knuth.__module__, '') + with self.assertRaisesRegex(TypeError, 'cannot be pickled'): + pickle.dumps(d_knuth) + class TestReplace(unittest.TestCase): def test(self): @dataclass(frozen=True) diff --git a/Misc/NEWS.d/next/Library/2018-12-30-21-00-56.bpo-35232.yMfv98.rst b/Misc/NEWS.d/next/Library/2018-12-30-21-00-56.bpo-35232.yMfv98.rst new file mode 100644 index 00000000000000..9535d3291cdbef --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-12-30-21-00-56.bpo-35232.yMfv98.rst @@ -0,0 +1,3 @@ +`dataclasses.make_dataclass` accepts two new keyword arguments `module` and +`qualname` in order to make created classes picklable. Patch contributed by +Rémi Lapeyre.