Skip to content

Commit 0a39730

Browse files
danielhollashugovk
andauthored
gh-137855: Lazy import inspect module in dataclasses (#144387)
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
1 parent e1384cf commit 0a39730

4 files changed

Lines changed: 57 additions & 18 deletions

File tree

Lib/dataclasses.py

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import sys
22
import types
3-
import inspect
43
import keyword
54
import itertools
65
import annotationlib
76
import abc
87
from reprlib import recursive_repr
98
lazy import copy
9+
lazy import inspect
1010
lazy import re
1111

1212

@@ -988,6 +988,28 @@ def _hash_exception(cls, fields, func_builder):
988988
# See https://bugs.python.org/issue32929#msg312829 for an if-statement
989989
# version of this table.
990990

991+
# A non-data descriptor to autogenerate class docstring
992+
# from the signature of its __init__ method on demand.
993+
# The primary reason is to be able to lazy import `inspect` module.
994+
class _AutoDocstring:
995+
996+
def __get__(self, _obj, cls):
997+
try:
998+
# In some cases fetching a signature is not possible.
999+
# But, we surely should not fail in this case.
1000+
text_sig = str(inspect.signature(
1001+
cls,
1002+
annotation_format=annotationlib.Format.FORWARDREF,
1003+
)).replace(' -> None', '')
1004+
except TypeError, ValueError:
1005+
text_sig = ''
1006+
1007+
doc = cls.__name__ + text_sig
1008+
setattr(cls, '__doc__', doc)
1009+
return doc
1010+
1011+
_auto_docstring = _AutoDocstring()
1012+
9911013

9921014
def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
9931015
match_args, kw_only, slots, weakref_slot):
@@ -1215,23 +1237,13 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
12151237
if hash_action:
12161238
cls.__hash__ = hash_action(cls, field_list, func_builder)
12171239

1218-
# Generate the methods and add them to the class. This needs to be done
1219-
# before the __doc__ logic below, since inspect will look at the __init__
1220-
# signature.
1240+
# Generate the methods and add them to the class.
12211241
func_builder.add_fns_to_class(cls)
12221242

12231243
if not getattr(cls, '__doc__'):
1224-
# Create a class doc-string.
1225-
try:
1226-
# In some cases fetching a signature is not possible.
1227-
# But, we surely should not fail in this case.
1228-
text_sig = str(inspect.signature(
1229-
cls,
1230-
annotation_format=annotationlib.Format.FORWARDREF,
1231-
)).replace(' -> None', '')
1232-
except (TypeError, ValueError):
1233-
text_sig = ''
1234-
cls.__doc__ = (cls.__name__ + text_sig)
1244+
# Create a class doc-string lazily via descriptor protocol
1245+
# to avoid importing `inspect` module.
1246+
cls.__doc__ = _auto_docstring
12351247

12361248
if match_args:
12371249
# I could probably compute this once.
@@ -1391,8 +1403,10 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
13911403
# make an update, since all closures for a class will share a
13921404
# given cell.
13931405
for member in newcls.__dict__.values():
1406+
13941407
# If this is a wrapped function, unwrap it.
1395-
member = inspect.unwrap(member)
1408+
if not isinstance(member, type) and hasattr(member, '__wrapped__'):
1409+
member = inspect.unwrap(member)
13961410

13971411
if isinstance(member, types.FunctionType):
13981412
if _update_func_cell_for__class__(member, cls, newcls):

Lib/test/test__colorize.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class TestImportTime(unittest.TestCase):
2828
@cpython_only
2929
def test_lazy_import(self):
3030
import_helper.ensure_lazy_imports(
31-
"_colorize", {"copy", "re"}
31+
"_colorize", {"copy", "re", "inspect"}
3232
)
3333

3434

Lib/test/test_dataclasses/__init__.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,21 @@
2727
import dataclasses # Needed for the string "dataclasses.InitVar[int]" to work as an annotation.
2828

2929
from test import support
30-
from test.support import import_helper
30+
from test.support import cpython_only, import_helper
3131

3232
# Just any custom exception we can catch.
3333
class CustomError(Exception): pass
3434

35+
36+
class TestImportTime(unittest.TestCase):
37+
38+
@cpython_only
39+
def test_lazy_import(self):
40+
import_helper.ensure_lazy_imports(
41+
"dataclasses", {"inspect", "re", "copy"}
42+
)
43+
44+
3545
class TestCase(unittest.TestCase):
3646
def test_no_fields(self):
3747
@dataclass
@@ -2309,6 +2319,20 @@ class C:
23092319

23102320
self.assertDocStrEqual(C.__doc__, "C()")
23112321

2322+
def test_docstring_slotted(self):
2323+
@dataclass(slots=True)
2324+
class C:
2325+
x: int
2326+
2327+
self.assertDocStrEqual(C.__doc__, "C(x:int)")
2328+
2329+
def test_docstring_recursive(self):
2330+
@dataclass()
2331+
class C:
2332+
x: list[C]
2333+
2334+
self.assertDocStrEqual(C.__doc__, "C(x:list[test.test_dataclasses.TestDocString.test_docstring_recursive.<locals>.C])")
2335+
23122336
def test_docstring_one_field(self):
23132337
@dataclass
23142338
class C:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Reduce the import time of :mod:`dataclasses` module by ~20%.

0 commit comments

Comments
 (0)