diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 730ced7299865e..238aa02ee0f55e 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -2,7 +2,7 @@ import sys import copy import types -import inspect +lazy import inspect import keyword import itertools import annotationlib @@ -982,6 +982,26 @@ def _hash_exception(cls, fields, func_builder): # See https://bugs.python.org/issue32929#msg312829 for an if-statement # version of this table. +# A non-data descriptor to autogenerate class docstring +# from the signature of its __init__ method on demand. +# The primary reason is to be able to lazy import `inspect` module. +class _AutoDocstring: + + def __get__(self, _obj, cls): + try: + # In some cases fetching a signature is not possible. + # But, we surely should not fail in this case. + text_sig = str(inspect.signature( + cls, + annotation_format=annotationlib.Format.FORWARDREF, + )).replace(' -> None', '') + except (TypeError, ValueError): + text_sig = '' + + doc = cls.__name__ + text_sig + setattr(cls, '__doc__', doc) + return doc + def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, match_args, kw_only, slots, weakref_slot): @@ -1209,23 +1229,13 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, if hash_action: cls.__hash__ = hash_action(cls, field_list, func_builder) - # Generate the methods and add them to the class. This needs to be done - # before the __doc__ logic below, since inspect will look at the __init__ - # signature. + # Generate the methods and add them to the class. func_builder.add_fns_to_class(cls) if not getattr(cls, '__doc__'): - # Create a class doc-string. - try: - # In some cases fetching a signature is not possible. - # But, we surely should not fail in this case. - text_sig = str(inspect.signature( - cls, - annotation_format=annotationlib.Format.FORWARDREF, - )).replace(' -> None', '') - except (TypeError, ValueError): - text_sig = '' - cls.__doc__ = (cls.__name__ + text_sig) + # Create a class doc-string lazily via descriptor protocol + # to avoid importing `inspect` module. + cls.__doc__ = _AutoDocstring() if match_args: # I could probably compute this once. @@ -1377,8 +1387,10 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields): # make an update, since all closures for a class will share a # given cell. for member in newcls.__dict__.values(): + # If this is a wrapped function, unwrap it. - member = inspect.unwrap(member) + if not isinstance(member, type) and hasattr(member, '__wrapped__'): + member = inspect.unwrap(member) if isinstance(member, types.FunctionType): if _update_func_cell_for__class__(member, cls, newcls): diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 3b335429b98500..2cf39911766ce9 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -27,11 +27,21 @@ import dataclasses # Needed for the string "dataclasses.InitVar[int]" to work as an annotation. from test import support -from test.support import import_helper +from test.support import cpython_only, import_helper # Just any custom exception we can catch. class CustomError(Exception): pass + +class TestImportTime(unittest.TestCase): + + @cpython_only + def test_lazy_import(self): + import_helper.ensure_lazy_imports( + "dataclasses", {"inspect"} + ) + + class TestCase(unittest.TestCase): def test_no_fields(self): @dataclass @@ -2295,6 +2305,13 @@ class C: self.assertDocStrEqual(C.__doc__, "C()") + def test_docstring_slotted(self): + @dataclass(slots=True) + class C: + x: int + + self.assertDocStrEqual(C.__doc__, "C(x:int)") + def test_docstring_one_field(self): @dataclass class C: diff --git a/Misc/NEWS.d/next/Library/2026-02-12-18-05-16.gh-issue-137855.2_PTbg.rst b/Misc/NEWS.d/next/Library/2026-02-12-18-05-16.gh-issue-137855.2_PTbg.rst new file mode 100644 index 00000000000000..586c7d3495ae26 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-12-18-05-16.gh-issue-137855.2_PTbg.rst @@ -0,0 +1 @@ +Reduce the import time of :mod:`dataclasses` module by ~20%.