Skip to content

Commit 37b168e

Browse files
committed
Defer inspect import and lazy-load converters/validators
Move `import inspect` from module level to first use in `_compat.py` and `_make.py`. Lazy-load `converters` and `validators` submodules via `__getattr__` in both `attr` and `attrs` packages. This avoids importing `inspect` (which pulls in ast, re, dis, tokenize, etc.) at `import attr` time, reducing import time by ~25%.
1 parent 3823062 commit 37b168e

6 files changed

Lines changed: 46 additions & 4 deletions

File tree

changelog.d/1547.change.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Deferred the import of `inspect` and the loading of the `validators` and `converters` submodules until first use, reducing `import attr`/`import attrs` time by ~25%.

src/attr/__init__.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
Classes Without Boilerplate
55
"""
66

7+
import sys
8+
79
from functools import partial
810
from typing import Callable, Literal, Protocol
911

10-
from . import converters, exceptions, filters, setters, validators
12+
from . import exceptions, filters, setters
1113
from ._cmp import cmp_using
1214
from ._config import get_run_validators, set_run_validators
1315
from ._funcs import asdict, assoc, astuple, has, resolve_types
@@ -78,13 +80,23 @@ class AttrsInstance(Protocol):
7880
]
7981

8082

83+
_LAZY_SUBMODULES = {"converters", "validators"}
84+
85+
8186
def _make_getattr(mod_name: str) -> Callable:
8287
"""
8388
Create a metadata proxy for packaging information that uses *mod_name* in
8489
its warnings and errors.
8590
"""
8691

8792
def __getattr__(name: str) -> str:
93+
if name in _LAZY_SUBMODULES:
94+
import importlib
95+
96+
mod = importlib.import_module(f".{name}", mod_name)
97+
sys.modules[mod_name].__dict__[name] = mod
98+
return mod
99+
88100
if name not in ("__version__", "__version_info__"):
89101
msg = f"module {mod_name} has no attribute {name}"
90102
raise AttributeError(msg)
@@ -102,3 +114,7 @@ def __getattr__(name: str) -> str:
102114

103115

104116
__getattr__ = _make_getattr(__name__)
117+
118+
119+
def __dir__() -> list[str]:
120+
return __all__

src/attr/_compat.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# SPDX-License-Identifier: MIT
22

3-
import inspect
43
import platform
54
import sys
65
import threading
@@ -46,6 +45,8 @@ class _AnnotationExtractor:
4645
__slots__ = ["sig"]
4746

4847
def __init__(self, callable):
48+
import inspect
49+
4950
try:
5051
self.sig = inspect.signature(callable)
5152
except (ValueError, TypeError): # inspect failed
@@ -55,6 +56,8 @@ def get_first_param_type(self):
5556
"""
5657
Return the type annotation of the first argument if it's not empty.
5758
"""
59+
import inspect
60+
5861
if not self.sig:
5962
return None
6063

@@ -68,6 +71,8 @@ def get_return_type(self):
6871
"""
6972
Return the return type if it's not empty.
7073
"""
74+
import inspect
75+
7176
if (
7277
self.sig
7378
and self.sig.return_annotation is not inspect.Signature.empty

src/attr/_make.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import contextlib
77
import copy
88
import enum
9-
import inspect
109
import itertools
1110
import linecache
1211
import sys
@@ -705,6 +704,8 @@ def __init__(
705704
if self._has_pre_init:
706705
# Check if the pre init method has more arguments than just `self`
707706
# We want to pass arguments if pre init expects arguments
707+
import inspect
708+
708709
pre_init_func = cls.__attrs_pre_init__
709710
pre_init_signature = inspect.signature(pre_init_func)
710711
self._pre_init_has_args = len(pre_init_signature.parameters) > 1
@@ -920,6 +921,8 @@ def _create_slots_class(self):
920921
# To know to update them.
921922
additional_closure_functions_to_update = []
922923
if cached_properties:
924+
import inspect
925+
923926
class_annotations = _get_annotations(self._cls)
924927
for name, func in cached_properties.items():
925928
# Add cached properties to names for slotting.

src/attrs/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from attr._make import ClassProps
2626
from attr._next_gen import asdict, astuple, inspect
2727

28-
from . import converters, exceptions, filters, setters, validators
28+
from . import exceptions, filters, setters
2929

3030

3131
__all__ = [
@@ -70,3 +70,7 @@
7070
]
7171

7272
__getattr__ = _make_getattr(__name__)
73+
74+
75+
def __dir__() -> list[str]:
76+
return __all__

tests/test_import.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# SPDX-License-Identifier: MIT
22

3+
import attr
4+
import attrs
5+
36

47
class TestImportStar:
58
def test_from_attr_import_star(self):
@@ -9,3 +12,13 @@ def test_from_attr_import_star(self):
912
# attr_import_star contains `from attr import *`, which cannot
1013
# be done here because *-imports are only allowed on module level.
1114
from . import attr_import_star # noqa: F401
15+
16+
17+
class TestDir:
18+
def test_attr_dir_includes_lazy_submodules(self):
19+
assert "converters" in dir(attr)
20+
assert "validators" in dir(attr)
21+
22+
def test_attrs_dir_includes_lazy_submodules(self):
23+
assert "converters" in dir(attrs)
24+
assert "validators" in dir(attrs)

0 commit comments

Comments
 (0)