Skip to content

Commit 4210688

Browse files
committed
Lazy import inspect in dataclasses module
1 parent 46d5106 commit 4210688

File tree

3 files changed

+67
-17
lines changed

3 files changed

+67
-17
lines changed

Lib/dataclasses.py

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import sys
33
import copy
44
import types
5-
import inspect
65
import keyword
76
import itertools
87
import annotationlib
@@ -432,6 +431,38 @@ def _tuple_str(obj_name, fields):
432431
# Note the trailing comma, needed if this turns out to be a 1-tuple.
433432
return f'({",".join([f"{obj_name}.{f.name}" for f in fields])},)'
434433

434+
# NOTE: This is a vendored copy of `inspect.unwrap` to speed up import time
435+
def _unwrap(func, *, stop=None):
436+
"""Get the object wrapped by *func*.
437+
438+
Follows the chain of :attr:`__wrapped__` attributes returning the last
439+
object in the chain.
440+
441+
*stop* is an optional callback accepting an object in the wrapper chain
442+
as its sole argument that allows the unwrapping to be terminated early if
443+
the callback returns a true value. If the callback never returns a true
444+
value, the last object in the chain is returned as usual. For example,
445+
:func:`signature` uses this to stop unwrapping if any object in the
446+
chain has a ``__signature__`` attribute defined.
447+
448+
:exc:`ValueError` is raised if a cycle is encountered.
449+
450+
"""
451+
f = func # remember the original func for error reporting
452+
# Memoise by id to tolerate non-hashable objects, but store objects to
453+
# ensure they aren't destroyed, which would allow their IDs to be reused.
454+
memo = {id(f): f}
455+
recursion_limit = sys.getrecursionlimit()
456+
while not isinstance(func, type) and hasattr(func, '__wrapped__'):
457+
if stop is not None and stop(func):
458+
break
459+
func = func.__wrapped__
460+
id_func = id(func)
461+
if (id_func in memo) or (len(memo) >= recursion_limit):
462+
raise ValueError('wrapper loop when unwrapping {!r}'.format(f))
463+
memo[id_func] = func
464+
return func
465+
435466

436467
class _FuncBuilder:
437468
def __init__(self, globals):
@@ -982,6 +1013,28 @@ def _hash_exception(cls, fields, func_builder):
9821013
# See https://bugs.python.org/issue32929#msg312829 for an if-statement
9831014
# version of this table.
9841015

1016+
class AutoDocstring:
1017+
"""A non-data descriptor to autogenerate class docstring
1018+
from the signature of its __init__ method.
1019+
"""
1020+
1021+
def __get__(self, _obj, cls):
1022+
import inspect
1023+
1024+
try:
1025+
# In some cases fetching a signature is not possible.
1026+
# But, we surely should not fail in this case.
1027+
text_sig = str(inspect.signature(
1028+
cls,
1029+
annotation_format=annotationlib.Format.FORWARDREF,
1030+
)).replace(' -> None', '')
1031+
except (TypeError, ValueError):
1032+
text_sig = ''
1033+
1034+
doc = cls.__name__ + text_sig
1035+
setattr(cls, '__doc__', doc)
1036+
return doc
1037+
9851038

9861039
def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
9871040
match_args, kw_only, slots, weakref_slot):
@@ -1209,23 +1262,13 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
12091262
if hash_action:
12101263
cls.__hash__ = hash_action(cls, field_list, func_builder)
12111264

1212-
# Generate the methods and add them to the class. This needs to be done
1213-
# before the __doc__ logic below, since inspect will look at the __init__
1214-
# signature.
1265+
# Generate the methods and add them to the class.
12151266
func_builder.add_fns_to_class(cls)
12161267

12171268
if not getattr(cls, '__doc__'):
1218-
# Create a class doc-string.
1219-
try:
1220-
# In some cases fetching a signature is not possible.
1221-
# But, we surely should not fail in this case.
1222-
text_sig = str(inspect.signature(
1223-
cls,
1224-
annotation_format=annotationlib.Format.FORWARDREF,
1225-
)).replace(' -> None', '')
1226-
except (TypeError, ValueError):
1227-
text_sig = ''
1228-
cls.__doc__ = (cls.__name__ + text_sig)
1269+
# Create a class doc-string lazily via descriptor protocol
1270+
# to avoid importing `inspect` module.
1271+
cls.__doc__ = AutoDocstring()
12291272

12301273
if match_args:
12311274
# I could probably compute this once.
@@ -1378,7 +1421,7 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
13781421
# given cell.
13791422
for member in newcls.__dict__.values():
13801423
# If this is a wrapped function, unwrap it.
1381-
member = inspect.unwrap(member)
1424+
member = _unwrap(member)
13821425

13831426
if isinstance(member, types.FunctionType):
13841427
if _update_func_cell_for__class__(member, cls, newcls):

Lib/inspect.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@
165165
from collections import namedtuple, OrderedDict
166166
from weakref import ref as make_weakref
167167

168-
# Create constants for the compiler flags in Include/code.h
168+
# Create constants for the compiler flags in Include/cpython/code.h
169169
# We try to get them from dis to avoid duplication
170170
mod_dict = globals()
171171
for k, v in dis.COMPILER_FLAG_NAMES.items():

Lib/test/test_dataclasses/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2295,6 +2295,13 @@ class C:
22952295

22962296
self.assertDocStrEqual(C.__doc__, "C()")
22972297

2298+
def test_docstring_slotted(self):
2299+
@dataclass(slots=True)
2300+
class C:
2301+
x: int
2302+
2303+
self.assertDocStrEqual(C.__doc__, "C(x:int)")
2304+
22982305
def test_docstring_one_field(self):
22992306
@dataclass
23002307
class C:

0 commit comments

Comments
 (0)