diff --git a/changelog.d/1547.change.md b/changelog.d/1547.change.md new file mode 100644 index 000000000..aa5eded16 --- /dev/null +++ b/changelog.d/1547.change.md @@ -0,0 +1 @@ +The import time for `import attr` and `import attrs` has been _significantly_ improved by making expensive imports lazy. diff --git a/src/attr/__init__.py b/src/attr/__init__.py index 5c6e0650b..3f6197c8b 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 sorted(set(globals()) | set(__all__)) diff --git a/src/attr/_compat.py b/src/attr/_compat.py index bc68ed9ea..37e714857 100644 --- a/src/attr/_compat.py +++ b/src/attr/_compat.py @@ -1,7 +1,5 @@ # SPDX-License-Identifier: MIT -import inspect -import platform import sys import threading @@ -9,7 +7,7 @@ from typing import _GenericAlias -PYPY = platform.python_implementation() == "PyPy" +PYPY = sys.implementation.name == "pypy" PY_3_10_PLUS = sys.version_info[:2] >= (3, 10) PY_3_11_PLUS = sys.version_info[:2] >= (3, 11) PY_3_12_PLUS = sys.version_info[:2] >= (3, 12) @@ -18,12 +16,12 @@ if PY_3_14_PLUS: - import annotationlib - # We request forward-ref annotations to not break in the presence of # forward references. def _get_annotations(cls): + import annotationlib + return annotationlib.get_annotations( cls, format=annotationlib.Format.FORWARDREF ) @@ -46,6 +44,8 @@ class _AnnotationExtractor: __slots__ = ["sig"] def __init__(self, callable): + import inspect + try: self.sig = inspect.signature(callable) except (ValueError, TypeError): # inspect failed @@ -55,6 +55,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 +70,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/_funcs.py b/src/attr/_funcs.py index 1adb50021..a0869d5df 100644 --- a/src/attr/_funcs.py +++ b/src/attr/_funcs.py @@ -1,8 +1,6 @@ # SPDX-License-Identifier: MIT -import copy - from ._compat import get_generic_base from ._make import _OBJ_SETATTR, NOTHING, fields from .exceptions import AttrsAttributeNotFoundError @@ -411,6 +409,8 @@ def assoc(inst, **changes): removed du to the slightly different approach compared to `attrs.evolve`, though. """ + import copy + new = copy.copy(inst) attrs = fields(inst.__class__) for k, v in changes.items(): diff --git a/src/attr/_make.py b/src/attr/_make.py index 6794464e9..04104d956 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -4,9 +4,7 @@ import abc import contextlib -import copy import enum -import inspect import itertools import linecache import sys @@ -705,6 +703,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 +920,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. @@ -2613,6 +2615,8 @@ def evolve(self, **changes): .. versionadded:: 20.3.0 """ + import copy + new = copy.copy(self) new._setattrs(changes.items()) diff --git a/src/attrs/__init__.py b/src/attrs/__init__.py index dc1ce4b97..db0e46383 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 sorted(set(globals()) | set(__all__)) diff --git a/tests/test_import.py b/tests/test_import.py index 9e90a5c11..5d5f1a072 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,23 @@ 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) + assert "__name__" in dir(attr) + assert "_make_getattr" 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) + assert "__name__" in dir(attrs) + assert "_make_getattr" in dir(attrs)