Thanks to visit codestin.com
Credit goes to github.com

Skip to content

gh-79413: Add module and qualname arguments to dataclasses.make_dataclass() #11371

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions Doc/library/dataclasses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
28 changes: 27 additions & 1 deletion Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand All @@ -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().
"""
Expand Down Expand Up @@ -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)

Expand Down
47 changes: 47 additions & 0 deletions Lib/test/test_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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__, '<unknown>')
with self.assertRaisesRegex(TypeError, 'cannot be pickled'):
pickle.dumps(d_knuth)

class TestReplace(unittest.TestCase):
def test(self):
@dataclass(frozen=True)
Expand Down
Original file line number Diff line number Diff line change
@@ -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.