From 58743b1e9d39640a1f6b0e26ad72d96340983a10 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 21 Feb 2023 11:56:44 +0300 Subject: [PATCH 1/4] gh-102103: add `module` argument to `dataclasses.make_dataclass` --- Doc/library/dataclasses.rst | 6 ++- Lib/dataclasses.py | 15 +++++++- Lib/test/test_dataclasses.py | 38 +++++++++++++++++++ ...-02-21-11-56-16.gh-issue-102103.Dj0WEj.rst | 2 + 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-02-21-11-56-16.gh-issue-102103.Dj0WEj.rst diff --git a/Doc/library/dataclasses.rst b/Doc/library/dataclasses.rst index 82faa7b77450fb..5f4dc25bfd7877 100644 --- a/Doc/library/dataclasses.rst +++ b/Doc/library/dataclasses.rst @@ -389,7 +389,7 @@ Module contents :func:`astuple` raises :exc:`TypeError` if ``obj`` 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, slots=False, weakref_slot=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, weakref_slot=False, module=None) Creates a new dataclass with name ``cls_name``, fields as defined in ``fields``, base classes as given in ``bases``, and initialized @@ -401,6 +401,10 @@ Module contents ``match_args``, ``kw_only``, ``slots``, and ``weakref_slot`` have the same meaning as they do in :func:`dataclass`. + If ``module`` is defined, the ``__module__`` attribute + of the dataclass is set to that value. + By default, it is set to the module name of the caller. + This function is not strictly required, because any Python mechanism for creating a new class with ``__annotations__`` can then apply the :func:`dataclass` function to convert that class to diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 5c0257eba186d1..b2a3871712b233 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1394,7 +1394,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, match_args=True, kw_only=False, slots=False, - weakref_slot=False): + weakref_slot=False, module=None): """Return a new dynamically created dataclass. The dataclass name will be 'cls_name'. 'fields' is an iterable @@ -1458,6 +1458,19 @@ def exec_body_callback(ns): # of generic dataclasses. cls = types.new_class(cls_name, bases, {}, exec_body_callback) + # For pickling to work, the __module__ variable needs to be set to the frame + # where the dataclass is created. + if module is None: + try: + module = sys._getframemodulename(1) or '__main__' + except AttributeError: + try: + module = sys._getframe(1).f_globals.get('__name__', '__main__') + except (AttributeError, ValueError): + pass + if module is not None: + cls.__module__ = module + # Apply the normal decorator. 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 81a36aa241acf7..24daece0bf0bf7 100644 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -3563,6 +3563,14 @@ def test_text_annotations(self): 'return': type(None)}) +ByMakeDataClass = make_dataclass('ByMakeDataClass', [('x', int)]) +ManualModuleMakeDataClass = make_dataclass('ManualModuleMakeDataClass', + [('x', int)], + module='test.test_dataclasses') +WrongNameMakeDataclass = make_dataclass('Wrong', [('x', int)]) +WrongModuleMakeDataclass = make_dataclass('Wrong', [('x', int)], + module='custom') + class TestMakeDataclass(unittest.TestCase): def test_simple(self): C = make_dataclass('C', @@ -3672,6 +3680,36 @@ def test_no_types(self): 'y': int, 'z': 'typing.Any'}) + def test_module_attr(self): + self.assertEqual(ByMakeDataClass.__module__, __name__) + self.assertEqual(ByMakeDataClass(1).__module__, __name__) + + Nested = make_dataclass('Nested', []) + self.assertEqual(Nested.__module__, __name__) + self.assertEqual(Nested().__module__, __name__) + + def test_pickle_support(self): + for klass in [ByMakeDataClass, ManualModuleMakeDataClass]: + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(proto=proto): + self.assertEqual( + pickle.loads(pickle.dumps(klass, proto)), + klass, + ) + self.assertEqual( + pickle.loads(pickle.dumps(klass(1), proto)), + klass(1), + ) + + def test_cannot_be_pickled(self): + for klass in [WrongNameMakeDataclass, WrongModuleMakeDataclass]: + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(proto=proto): + with self.assertRaises(pickle.PickleError): + pickle.dumps(klass, proto) + with self.assertRaises(pickle.PickleError): + pickle.dumps(klass(1), proto) + def test_invalid_type_specification(self): for bad_field in [(), (1, 2, 3, 4), diff --git a/Misc/NEWS.d/next/Library/2023-02-21-11-56-16.gh-issue-102103.Dj0WEj.rst b/Misc/NEWS.d/next/Library/2023-02-21-11-56-16.gh-issue-102103.Dj0WEj.rst new file mode 100644 index 00000000000000..feba433f5bee89 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-02-21-11-56-16.gh-issue-102103.Dj0WEj.rst @@ -0,0 +1,2 @@ +Add ``module`` argument to :func:`dataclasses.make_dataclass` and make +classes produced by it pickleable. From 3cc2c754b1fd68471f35bb44a16638cf0d7937a7 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 21 Feb 2023 11:58:36 +0300 Subject: [PATCH 2/4] Fix typo --- Lib/test/test_dataclasses.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index 24daece0bf0bf7..4029a132a89669 100644 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -3568,7 +3568,8 @@ def test_text_annotations(self): [('x', int)], module='test.test_dataclasses') WrongNameMakeDataclass = make_dataclass('Wrong', [('x', int)]) -WrongModuleMakeDataclass = make_dataclass('Wrong', [('x', int)], +WrongModuleMakeDataclass = make_dataclass('WrongModuleMakeDataclass', + [('x', int)], module='custom') class TestMakeDataclass(unittest.TestCase): From 7a7783965b426377ef2565f0676dc88c87b8dabb Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Wed, 22 Feb 2023 23:17:20 +0300 Subject: [PATCH 3/4] Update Lib/test/test_dataclasses.py Co-authored-by: Carl Meyer --- Lib/test/test_dataclasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index 4029a132a89669..f02964911d2eb1 100644 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -3684,7 +3684,7 @@ def test_no_types(self): def test_module_attr(self): self.assertEqual(ByMakeDataClass.__module__, __name__) self.assertEqual(ByMakeDataClass(1).__module__, __name__) - +self.assertEqual(WrongModuleMakeDataclass.__module__, "custom") Nested = make_dataclass('Nested', []) self.assertEqual(Nested.__module__, __name__) self.assertEqual(Nested().__module__, __name__) From 2833e1c2bde99cc4b723a15a970dcf41d3c494ca Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Thu, 23 Feb 2023 00:00:18 +0300 Subject: [PATCH 4/4] Update Lib/test/test_dataclasses.py --- Lib/test/test_dataclasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index f02964911d2eb1..7d1db4c6a17baf 100644 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -3684,7 +3684,7 @@ def test_no_types(self): def test_module_attr(self): self.assertEqual(ByMakeDataClass.__module__, __name__) self.assertEqual(ByMakeDataClass(1).__module__, __name__) -self.assertEqual(WrongModuleMakeDataclass.__module__, "custom") + self.assertEqual(WrongModuleMakeDataclass.__module__, "custom") Nested = make_dataclass('Nested', []) self.assertEqual(Nested.__module__, __name__) self.assertEqual(Nested().__module__, __name__)