diff --git a/changelog.d/1547.change.md b/changelog.d/1547.change.md new file mode 100644 index 000000000..ae2af6204 --- /dev/null +++ b/changelog.d/1547.change.md @@ -0,0 +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%. diff --git a/src/attr/__init__.py b/src/attr/__init__.py index 5c6e0650b..e4f69fabe 100644 --- a/src/attr/__init__.py +++ b/src/attr/__init__.py @@ -4,10 +4,12 @@ Classes Without Boilerplate """ +import sys + from functools import partial from typing import Callable, Literal, Protocol -from . import converters, exceptions, filters, setters, validators +from . import exceptions, filters, setters from ._cmp import cmp_using from ._config import get_run_validators, set_run_validators from ._funcs import asdict, assoc, astuple, has, resolve_types @@ -78,6 +80,9 @@ class AttrsInstance(Protocol): ] +_LAZY_SUBMODULES = {"converters", "validators"} + + def _make_getattr(mod_name: str) -> Callable: """ Create a metadata proxy for packaging information that uses *mod_name* in @@ -85,6 +90,13 @@ def _make_getattr(mod_name: str) -> Callable: """ def __getattr__(name: str) -> str: + if name in _LAZY_SUBMODULES: + import importlib + + mod = importlib.import_module(f".{name}", mod_name) + sys.modules[mod_name].__dict__[name] = mod + return mod + if name not in ("__version__", "__version_info__"): msg = f"module {mod_name} has no attribute {name}" raise AttributeError(msg) @@ -102,3 +114,7 @@ def __getattr__(name: str) -> str: __getattr__ = _make_getattr(__name__) + + +def __dir__() -> list[str]: + return __all__ diff --git a/src/attr/_compat.py b/src/attr/_compat.py index bc68ed9ea..e25a4a87a 100644 --- a/src/attr/_compat.py +++ b/src/attr/_compat.py @@ -1,6 +1,5 @@ # SPDX-License-Identifier: MIT -import inspect import platform import sys import threading @@ -46,6 +45,8 @@ class _AnnotationExtractor: __slots__ = ["sig"] def __init__(self, callable): + import inspect + try: self.sig = inspect.signature(callable) except (ValueError, TypeError): # inspect failed @@ -55,6 +56,8 @@ def get_first_param_type(self): """ Return the type annotation of the first argument if it's not empty. """ + import inspect + if not self.sig: return None @@ -68,6 +71,8 @@ def get_return_type(self): """ Return the return type if it's not empty. """ + import inspect + if ( self.sig and self.sig.return_annotation is not inspect.Signature.empty diff --git a/src/attr/_make.py b/src/attr/_make.py index 793bfd89d..89852f351 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -6,7 +6,6 @@ import contextlib import copy import enum -import inspect import itertools import linecache import sys @@ -705,6 +704,8 @@ def __init__( if self._has_pre_init: # Check if the pre init method has more arguments than just `self` # We want to pass arguments if pre init expects arguments + import inspect + pre_init_func = cls.__attrs_pre_init__ pre_init_signature = inspect.signature(pre_init_func) self._pre_init_has_args = len(pre_init_signature.parameters) > 1 @@ -920,6 +921,8 @@ def _create_slots_class(self): # To know to update them. additional_closure_functions_to_update = [] if cached_properties: + import inspect + class_annotations = _get_annotations(self._cls) for name, func in cached_properties.items(): # Add cached properties to names for slotting. diff --git a/src/attrs/__init__.py b/src/attrs/__init__.py index dc1ce4b97..9aaa225ee 100644 --- a/src/attrs/__init__.py +++ b/src/attrs/__init__.py @@ -25,7 +25,7 @@ from attr._make import ClassProps from attr._next_gen import asdict, astuple, inspect -from . import converters, exceptions, filters, setters, validators +from . import exceptions, filters, setters __all__ = [ @@ -70,3 +70,7 @@ ] __getattr__ = _make_getattr(__name__) + + +def __dir__() -> list[str]: + return __all__ diff --git a/tests/test_import.py b/tests/test_import.py index 9e90a5c11..922d7a800 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -1,5 +1,8 @@ # SPDX-License-Identifier: MIT +import attr +import attrs + class TestImportStar: def test_from_attr_import_star(self): @@ -9,3 +12,19 @@ def test_from_attr_import_star(self): # attr_import_star contains `from attr import *`, which cannot # be done here because *-imports are only allowed on module level. from . import attr_import_star # noqa: F401 + + +class TestDir: + def test_attr_dir_includes_lazy_submodules(self): + """ + converters and validators are listed in dir(attr). + """ + assert "converters" in dir(attr) + assert "validators" in dir(attr) + + def test_attrs_dir_includes_lazy_submodules(self): + """ + converters and validators are listed in dir(attrs). + """ + assert "converters" in dir(attrs) + assert "validators" in dir(attrs)