]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-137855: Lazy import `inspect` module in dataclasses (#144387)
authorDaniel Hollas <daniel.hollas@bristol.ac.uk>
Sun, 26 Apr 2026 05:57:38 +0000 (06:57 +0100)
committerGitHub <noreply@github.com>
Sun, 26 Apr 2026 05:57:38 +0000 (22:57 -0700)
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Lib/dataclasses.py
Lib/test/test__colorize.py
Lib/test/test_dataclasses/__init__.py
Misc/NEWS.d/next/Library/2026-02-12-18-05-16.gh-issue-137855.2_PTbg.rst [new file with mode: 0644]

index 988edfed6f4dcbd97a881fcf34494b57c40ef75f..df192763c5bd15a8595b17211bc9d4544bd620d0 100644 (file)
@@ -1,12 +1,12 @@
 import sys
 import types
-import inspect
 import keyword
 import itertools
 import annotationlib
 import abc
 from reprlib import recursive_repr
 lazy import copy
+lazy import inspect
 lazy import re
 
 
@@ -988,6 +988,28 @@ _hash_action = {(False, False, False, False): None,
 # 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
+
+_auto_docstring = _AutoDocstring()
+
 
 def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
                    match_args, kw_only, slots, weakref_slot):
@@ -1215,23 +1237,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__ = _auto_docstring
 
     if match_args:
         # I could probably compute this once.
@@ -1391,8 +1403,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):
index 0353ff7530b92a7c8499e06520b5860778f08aa0..48fa52bfd5672cb24ec76aa54b820169d7a0fc87 100644 (file)
@@ -28,7 +28,7 @@ class TestImportTime(unittest.TestCase):
     @cpython_only
     def test_lazy_import(self):
         import_helper.ensure_lazy_imports(
-            "_colorize", {"copy", "re"}
+            "_colorize", {"copy", "re", "inspect"}
         )
 
 
index 6ff82b8810abed9baf071040fcf30f5395e3865e..5eec9e23cd414b62921a813d16325f192fcdc438 100644 (file)
@@ -27,11 +27,21 @@ import typing       # Needed for the string "typing.ClassVar[int]" to work as an
 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", "re", "copy"}
+        )
+
+
 class TestCase(unittest.TestCase):
     def test_no_fields(self):
         @dataclass
@@ -2309,6 +2319,20 @@ class TestDocString(unittest.TestCase):
 
         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_recursive(self):
+        @dataclass()
+        class C:
+            x: list[C]
+
+        self.assertDocStrEqual(C.__doc__, "C(x:list[test.test_dataclasses.TestDocString.test_docstring_recursive.<locals>.C])")
+
     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 (file)
index 0000000..586c7d3
--- /dev/null
@@ -0,0 +1 @@
+Reduce the import time of :mod:`dataclasses` module by ~20%.