Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1547.change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The import time for `import attr` and `import attrs` has been _significantly_ improved by making expensive imports lazy.
18 changes: 17 additions & 1 deletion src/attr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -78,13 +80,23 @@ 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
its warnings and errors.
"""

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)
Expand All @@ -102,3 +114,7 @@ def __getattr__(name: str) -> str:


__getattr__ = _make_getattr(__name__)


def __dir__() -> list[str]:
return sorted(set(globals()) | set(__all__))
14 changes: 9 additions & 5 deletions src/attr/_compat.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
# SPDX-License-Identifier: MIT

import inspect
import platform
import sys
import threading

from collections.abc import Mapping, Sequence # noqa: F401
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)
Expand All @@ -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
)
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/attr/_funcs.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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():
Expand Down
8 changes: 6 additions & 2 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@

import abc
import contextlib
import copy
import enum
import inspect
import itertools
import linecache
import sys
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -2613,6 +2615,8 @@ def evolve(self, **changes):

.. versionadded:: 20.3.0
"""
import copy

new = copy.copy(self)

new._setattrs(changes.items())
Expand Down
6 changes: 5 additions & 1 deletion src/attrs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = [
Expand Down Expand Up @@ -70,3 +70,7 @@
]

__getattr__ = _make_getattr(__name__)


def __dir__() -> list[str]:
return sorted(set(globals()) | set(__all__))
23 changes: 23 additions & 0 deletions tests/test_import.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# SPDX-License-Identifier: MIT

import attr
import attrs


class TestImportStar:
def test_from_attr_import_star(self):
Expand All @@ -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)