1"""Implements deferred imports using documented Python protocols."""
2
3from __future__ import annotations
4
5import builtins
6import contextlib
7import importlib.util
8import operator
9import threading
10import types
11
12# Type checkers recognize this flag without importing typing at runtime.
13TYPE_CHECKING = False
14if TYPE_CHECKING:
15 from collections.abc import Callable, Generator, Mapping, Sequence
16 from typing import Any, Final
17
18
19class _Request:
20 __slots__ = ("name", "namespace", "fromlist", "attribute", "path")
21
22 name: str
23 namespace: dict[str, Any]
24 fromlist: tuple[str, ...]
25 attribute: str | None
26 path: tuple[str, ...]
27
28 def __init__(
29 self,
30 name: str,
31 namespace: dict[str, Any],
32 fromlist: tuple[str, ...],
33 attribute: str | None = None,
34 path: tuple[str, ...] = (),
35 ) -> None:
36 object.__setattr__(self, "name", name)
37 object.__setattr__(self, "namespace", namespace)
38 object.__setattr__(self, "fromlist", fromlist)
39 object.__setattr__(self, "attribute", attribute)
40 object.__setattr__(self, "path", path)
41
42 def __setattr__(self, name: str, value: Any) -> None:
43 raise AttributeError("request is immutable")
44
45 def __delattr__(self, name: str) -> None:
46 raise AttributeError("request is immutable")
47
48 def load(self) -> Any:
49 with _HOOK.suspended():
50 module = builtins.__import__(
51 self.name, self.namespace, self.namespace, self.fromlist, 0
52 )
53 if self.attribute is not None:
54 try:
55 return getattr(module, self.attribute)
56 except AttributeError as exc:
57 raise ImportError(
58 f"cannot import name {self.attribute!r} "
59 f"from {module.__name__!r}"
60 ) from exc
61 for component in self.path:
62 module = getattr(module, component)
63 return module
64
65
66class _Proxy:
67 def __init__(self, request: _Request) -> None:
68 object.__setattr__(self, "_request", request)
69 object.__setattr__(self, "_children", {})
70 object.__setattr__(self, "_namespaces", [request.namespace])
71 object.__setattr__(self, "_lock", threading.RLock())
72 object.__setattr__(self, "_resolved", ())
73 object.__setattr__(self, "_resolving", threading.local())
74
75 def _resolve(self) -> Any:
76 resolved = object.__getattribute__(self, "_resolved")
77 if resolved:
78 return resolved[0]
79 resolving = object.__getattribute__(self, "_resolving")
80 request = object.__getattribute__(self, "_request")
81 if getattr(resolving, "active", False):
82 raise ImportError(f"circular lazy import of {request.name!r}")
83 resolving.active = True
84 try:
85 value = request.load()
86 finally:
87 resolving.active = False
88 # Never hold a proxy lock while running import hooks or module code:
89 # another thread importing that module may need this same proxy.
90 with object.__getattribute__(self, "_lock"):
91 resolved = object.__getattribute__(self, "_resolved")
92 if resolved:
93 return resolved[0]
94 object.__setattr__(self, "_resolved", (value,))
95 if isinstance(value, types.ModuleType):
96 for name, child in object.__getattribute__(self, "_children").items():
97 if name not in vars(value):
98 vars(value)[name] = child
99 object.__getattribute__(child, "_namespaces").append(
100 vars(value)
101 )
102 for namespace in object.__getattribute__(self, "_namespaces"):
103 for name, binding in tuple(namespace.items()):
104 if binding is self and namespace.get(name) is self:
105 namespace[name] = value
106 return value
107
108 def __getattribute__(self, name: str) -> Any:
109 if name == "__mro_entries__":
110 return object.__getattribute__(self, name)
111 resolved = object.__getattribute__(self, "_resolved")
112 if resolved:
113 return getattr(resolved[0], name)
114 children = object.__getattribute__(self, "_children")
115 if name in children:
116 return children[name]
117 return getattr(_Proxy._resolve(self), name)
118
119 def __setattr__(self, name: str, value: Any) -> None:
120 setattr(_Proxy._resolve(self), name, value)
121
122 def __delattr__(self, name: str) -> None:
123 delattr(_Proxy._resolve(self), name)
124
125 def __call__(self, *args: Any, **kwargs: Any) -> Any:
126 target = _Proxy._resolve(self)
127 args = tuple(resolve(arg) for arg in args)
128 kwargs = {name: resolve(value) for name, value in kwargs.items()}
129 return target(*args, **kwargs)
130
131 def __mro_entries__(self, bases: tuple[type, ...]) -> tuple[type, ...]:
132 return (_Proxy._resolve(self),)
133
134
135# Special methods are looked up on the type, bypassing __getattribute__.
136# Keep the supported protocol surface explicit, including reflected operators.
137_PROTOCOLS: Final = (
138 "repr",
139 "str",
140 "bytes",
141 "format",
142 "bool",
143 "hash",
144 "len",
145 "iter",
146 "next",
147 "reversed",
148 "contains",
149 "getitem",
150 "setitem",
151 "delitem",
152 "enter",
153 "exit",
154 "await",
155 "aiter",
156 "anext",
157 "aenter",
158 "aexit",
159 "int",
160 "float",
161 "complex",
162 "index",
163 "round",
164 "trunc",
165 "floor",
166 "ceil",
167 "neg",
168 "pos",
169 "abs",
170 "invert",
171 "lt",
172 "le",
173 "eq",
174 "ne",
175 "gt",
176 "ge",
177 "add",
178 "sub",
179 "mul",
180 "matmul",
181 "truediv",
182 "floordiv",
183 "mod",
184 "divmod",
185 "pow",
186 "lshift",
187 "rshift",
188 "and",
189 "xor",
190 "or",
191 "radd",
192 "rsub",
193 "rmul",
194 "rmatmul",
195 "rtruediv",
196 "rfloordiv",
197 "rmod",
198 "rdivmod",
199 "rpow",
200 "rlshift",
201 "rrshift",
202 "rand",
203 "rxor",
204 "ror",
205 "iadd",
206 "isub",
207 "imul",
208 "imatmul",
209 "itruediv",
210 "ifloordiv",
211 "imod",
212 "ipow",
213 "ilshift",
214 "irshift",
215 "iand",
216 "ixor",
217 "ior",
218)
219
220
221_OPERATIONS: Final[Mapping[str, Callable[..., Any]]] = {
222 "__bool__": bool,
223 "__str__": str,
224 "__repr__": repr,
225 "__bytes__": bytes,
226 "__format__": format,
227 "__hash__": hash,
228 "__len__": len,
229 "__iter__": iter,
230 "__next__": next,
231 "__reversed__": reversed,
232 "__int__": int,
233 "__float__": float,
234 "__complex__": complex,
235 "__round__": round,
236 "__abs__": abs,
237 "__contains__": operator.contains,
238 "__pow__": pow,
239 "__divmod__": divmod,
240}
241
242
243_REFLECTED: Final[Mapping[str, str]] = {
244 f"__r{name}__": f"__{name}__"
245 for name in (
246 "add",
247 "sub",
248 "mul",
249 "matmul",
250 "truediv",
251 "floordiv",
252 "mod",
253 "divmod",
254 "pow",
255 "lshift",
256 "rshift",
257 "and",
258 "xor",
259 "or",
260 )
261}
262
263
264def _forward(method: str) -> Callable[..., Any]:
265 def operation(self: _Proxy, *args: Any, **kwargs: Any) -> Any:
266 value = _Proxy._resolve(self)
267 args = tuple(resolve(arg) for arg in args)
268 operation = _OPERATIONS.get(method)
269 if operation is not None:
270 return operation(value, *args, **kwargs)
271 operation = getattr(operator, method, None)
272 if operation is not None:
273 return operation(value, *args, **kwargs)
274 if method in _REFLECTED:
275 operation = _OPERATIONS.get(_REFLECTED[method])
276 if operation is None:
277 operation = getattr(operator, _REFLECTED[method])
278 return operation(args[0], value, *args[1:], **kwargs)
279 return getattr(type(value), method)(value, *args, **kwargs)
280
281 return operation
282
283
284for _protocol in _PROTOCOLS:
285 setattr(_Proxy, f"__{_protocol}__", _forward(f"__{_protocol}__"))
286
287
288def resolve(value: Any, /) -> Any:
289 """Returns the imported object, or the argument if it isn't a lazy proxy.
290
291 Resolves fallback proxies explicitly for identity checks, type checks and
292 APIs that require concrete objects. Failed imports can be retried.
293 """
294 if type(value) is _Proxy:
295 return _Proxy._resolve(value)
296 return value
297
298
299class _FromImport:
300 def __init__(self, request: _Request) -> None:
301 self._request = request
302
303 def __getattribute__(self, name: str) -> _Proxy:
304 request = object.__getattribute__(self, "_request")
305 return _Proxy(
306 _Request(
307 request.name,
308 request.namespace,
309 (name,),
310 attribute=name,
311 path=request.path,
312 )
313 )
314
315
316class _ThreadState(threading.local):
317 def __init__(self) -> None:
318 self.depth = 0
319 self.suspensions = 0
320
321
322class _ImportHook:
323 def __init__(self) -> None:
324 self._lock = threading.RLock()
325 self._state = _ThreadState()
326 self._users = 0
327 self._previous: Callable[..., Any] = builtins.__import__
328 # Store the bound method so installation and restoration use identity.
329 self._callback = self._import
330
331 @contextlib.contextmanager
332 def suspended(self) -> Generator[None, None, None]:
333 self._state.suspensions += 1
334 try:
335 yield
336 finally:
337 self._state.suspensions -= 1
338
339 @contextlib.contextmanager
340 def activate(self) -> Generator[None, None, None]:
341 with self._lock:
342 if self._users == 0:
343 self._previous = builtins.__import__
344 vars(builtins)["__import__"] = self._callback
345 self._users += 1
346 self._state.depth += 1
347 try:
348 yield
349 finally:
350 self._state.depth -= 1
351 with self._lock:
352 self._users -= 1
353 if self._users == 0 and builtins.__import__ is self._callback:
354 builtins.__import__ = self._previous
355
356 def _import(
357 self,
358 name: str,
359 globals: dict[str, Any] | None = None,
360 locals: Mapping[str, Any] | None = None,
361 fromlist: Sequence[str] | None = (),
362 level: int = 0,
363 ) -> Any:
364 state = self._state
365 if (
366 state.depth
367 and not state.suspensions
368 and globals is not None
369 and "*" not in (fromlist or ())
370 and name != "__future__"
371 ):
372 absolute = name
373 if level:
374 package = globals.get("__package__")
375 if package is None:
376 spec = globals.get("__spec__")
377 if spec is not None:
378 package = spec.parent
379 else:
380 package = globals.get("__name__", "")
381 if "__path__" not in globals:
382 package = package.rpartition(".")[0]
383 absolute = importlib.util.resolve_name("." * level + name, package)
384 # Membership may run user code that imports its own dependencies.
385 with self.suspended():
386 selected = absolute in globals.get("__lazy_modules__", ())
387 if selected:
388 request = _Request(absolute, globals, tuple(fromlist or ()))
389 if fromlist:
390 return _FromImport(request)
391 existing = globals.get(name.partition(".")[0])
392 root = _Proxy(request)
393 if type(existing) is _Proxy:
394 object.__getattribute__(root, "_children").update(
395 object.__getattribute__(existing, "_children")
396 )
397 parent = root
398 components = name.split(".")
399 for index, component in enumerate(components[1:], 1):
400 children = object.__getattribute__(parent, "_children")
401 child = _Proxy(
402 _Request(
403 request.name,
404 request.namespace,
405 request.fromlist,
406 attribute=request.attribute,
407 path=tuple(components[1 : index + 1]),
408 )
409 )
410 if component in children:
411 object.__getattribute__(child, "_children").update(
412 object.__getattribute__(children[component], "_children")
413 )
414 object.__getattribute__(parent, "_children")[component] = child
415 parent = child
416 return root
417 with self.suspended():
418 return self._previous(name, globals, locals, fromlist, level)
419
420
421_HOOK: Final = _ImportHook()
422
423
424def lazy_importer() -> contextlib.AbstractContextManager[None]:
425 """Returns a context that enables the importing module's __lazy_modules__.
426
427 Each import checks membership in its module's current declaration. The
428 fallback context is single-use and applies to imports on the current thread.
429 Module-level blocks containing only imports avoid differences from native
430 lazy imports described in the README.
431 """
432 return _HOOK.activate()