From 5a28ba0091935fb710dc8c93efeb9a456e72033d Mon Sep 17 00:00:00 2001 From: Kevin Turcios Date: Fri, 31 Jul 2026 02:26:00 -0500 Subject: [PATCH 1/3] Defer imports and preserve module introspection --- changelog.d/1547.change.md | 1 + src/attr/__init__.py | 18 +++++++++++++++++- src/attr/_compat.py | 7 ++++++- src/attr/_make.py | 5 ++++- src/attrs/__init__.py | 6 +++++- tests/test_import.py | 23 +++++++++++++++++++++++ 6 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 changelog.d/1547.change.md 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..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..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 6794464e9..6143129fb 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..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) From a858aac08ae9f4be3ac710196bcacef9102eb065 Mon Sep 17 00:00:00 2001 From: Kevin Turcios Date: Fri, 31 Jul 2026 02:30:11 -0500 Subject: [PATCH 2/3] Reduce cold import overhead --- src/attr/_compat.py | 7 +++---- src/attr/_funcs.py | 4 ++-- src/attr/_make.py | 3 ++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/attr/_compat.py b/src/attr/_compat.py index e25a4a87a..37e714857 100644 --- a/src/attr/_compat.py +++ b/src/attr/_compat.py @@ -1,6 +1,5 @@ # SPDX-License-Identifier: MIT -import platform import sys import threading @@ -8,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) @@ -17,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 ) 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 6143129fb..04104d956 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -4,7 +4,6 @@ import abc import contextlib -import copy import enum import itertools import linecache @@ -2616,6 +2615,8 @@ def evolve(self, **changes): .. versionadded:: 20.3.0 """ + import copy + new = copy.copy(self) new._setattrs(changes.items()) From 7348f36d68ec74894c9c1f5538a903d87a03fd7f Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Fri, 31 Jul 2026 10:06:27 +0200 Subject: [PATCH 3/3] Make more use-centric --- changelog.d/1547.change.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/1547.change.md b/changelog.d/1547.change.md index ae2af6204..aa5eded16 100644 --- a/changelog.d/1547.change.md +++ b/changelog.d/1547.change.md @@ -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%. +The import time for `import attr` and `import attrs` has been _significantly_ improved by making expensive imports lazy.