From bad6a9c4e0da492730a770bdba8ee0fc84d36589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Lapeyre?= Date: Sun, 30 Dec 2018 21:01:59 +0100 Subject: [PATCH] Add module and qualname arguments to dataclasses.make_dataclass() --- Doc/library/dataclasses.rst | 21 +++++++-- Lib/dataclasses.py | 28 ++++++++++- Lib/test/test_dataclasses.py | 47 +++++++++++++++++++ .../2018-12-30-21-00-56.bpo-35232.yMfv98.rst | 3 ++ 4 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2018-12-30-21-00-56.bpo-35232.yMfv98.rst 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. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy