1from __future__ import annotations
2
3import warnings
4from collections.abc import Iterable
5from typing import Any
6
7
8__all__ = ["lazy_import"]
9
10
11def import_name(name: str, source: str, namespace: dict[str, Any]) -> Any:
12 """
13 Import ``name`` from ``source`` in ``namespace``.
14
15 There are two use cases:
16
17 - ``name`` is an object defined in ``source``;
18 - ``name`` is a submodule of ``source``.
19
20 Neither :func:`__import__` nor :func:`~importlib.import_module` does
21 exactly this. :func:`__import__` is closer to the intended behavior.
22
23 """
24 level = 0
25 while source[level] == ".":
26 level += 1
27 assert level < len(source), "importing from parent isn't supported"
28 module = __import__(source[level:], namespace, None, [name], level)
29 return getattr(module, name)
30
31
32def lazy_import(
33 namespace: dict[str, Any],
34 aliases: dict[str, str] | None = None,
35 deprecated_aliases: dict[str, str] | None = None,
36) -> None:
37 """
38 Provide lazy, module-level imports.
39
40 Typical use::
41
42 __getattr__, __dir__ = lazy_import(
43 globals(),
44 aliases={
45 "<name>": "<source module>",
46 ...
47 },
48 deprecated_aliases={
49 ...,
50 }
51 )
52
53 This function defines ``__getattr__`` and ``__dir__`` per :pep:`562`.
54
55 """
56 if aliases is None:
57 aliases = {}
58 if deprecated_aliases is None:
59 deprecated_aliases = {}
60
61 namespace_set = set(namespace)
62 aliases_set = set(aliases)
63 deprecated_aliases_set = set(deprecated_aliases)
64
65 assert not namespace_set & aliases_set, "namespace conflict"
66 assert not namespace_set & deprecated_aliases_set, "namespace conflict"
67 assert not aliases_set & deprecated_aliases_set, "namespace conflict"
68
69 package = namespace["__name__"]
70
71 def __getattr__(name: str) -> Any:
72 assert aliases is not None # mypy cannot figure this out
73 try:
74 source = aliases[name]
75 except KeyError:
76 pass
77 else:
78 return import_name(name, source, namespace)
79
80 assert deprecated_aliases is not None # mypy cannot figure this out
81 try:
82 source = deprecated_aliases[name]
83 except KeyError:
84 pass
85 else:
86 warnings.warn(
87 f"{package}.{name} is deprecated",
88 DeprecationWarning,
89 stacklevel=2,
90 )
91 return import_name(name, source, namespace)
92
93 raise AttributeError(f"module {package!r} has no attribute {name!r}")
94
95 namespace["__getattr__"] = __getattr__
96
97 def __dir__() -> Iterable[str]:
98 return sorted(namespace_set | aliases_set | deprecated_aliases_set)
99
100 namespace["__dir__"] = __dir__