|
5 | 5 | Makes it easy to load subpackages and functions on demand.
|
6 | 6 | """
|
7 | 7 | import ast
|
| 8 | +import builtins |
8 | 9 | import importlib
|
9 | 10 | import importlib.util
|
10 | 11 | import inspect
|
11 | 12 | import os
|
12 | 13 | import sys
|
13 | 14 | import types
|
14 | 15 | import warnings
|
| 16 | +from contextvars import ContextVar |
| 17 | +from importlib.abc import MetaPathFinder |
| 18 | +from importlib.machinery import ModuleSpec |
| 19 | +from typing import Sequence |
15 | 20 |
|
16 | 21 | __all__ = ["attach", "load", "attach_stub"]
|
17 | 22 |
|
| 23 | +inside_context_manager: ContextVar[bool] = ContextVar( |
| 24 | + 'inside_context_manager', |
| 25 | + default=False, |
| 26 | +) |
| 27 | +searching_spec: ContextVar[bool] = ContextVar( |
| 28 | + 'searching_spec', |
| 29 | + default=False, |
| 30 | +) |
| 31 | + |
18 | 32 |
|
19 | 33 | def attach(package_name, submodules=None, submod_attrs=None):
|
20 | 34 | """Attach lazily loaded submodules, functions, or other attributes.
|
@@ -257,15 +271,72 @@ def attach_stub(package_name: str, filename: str):
|
257 | 271 | incorrectly (e.g. if it contains an relative import from outside of the module)
|
258 | 272 | """
|
259 | 273 | stubfile = (
|
260 |
| - filename if filename.endswith("i") else f"{os.path.splitext(filename)[0]}.pyi" |
| 274 | + filename if filename.endswith("i") |
| 275 | + else f"{os.path.splitext(filename)[0]}.pyi" |
261 | 276 | )
|
262 | 277 |
|
263 | 278 | if not os.path.exists(stubfile):
|
264 |
| - raise ValueError(f"Cannot load imports from non-existent stub {stubfile!r}") |
| 279 | + raise ValueError( |
| 280 | + f"Cannot load imports from non-existent stub {stubfile!r}", |
| 281 | + ) |
265 | 282 |
|
266 | 283 | with open(stubfile) as f:
|
267 | 284 | stub_node = ast.parse(f.read())
|
268 | 285 |
|
269 | 286 | visitor = _StubVisitor()
|
270 | 287 | visitor.visit(stub_node)
|
271 | 288 | return attach(package_name, visitor._submodules, visitor._submod_attrs)
|
| 289 | + |
| 290 | + |
| 291 | +class LazyFinder(MetaPathFinder): |
| 292 | + |
| 293 | + def find_spec( |
| 294 | + self, |
| 295 | + fullname: str, |
| 296 | + path: Sequence[str] | None, |
| 297 | + target: types.ModuleType | None = ..., |
| 298 | + / , |
| 299 | + ) -> ModuleSpec | None: |
| 300 | + if not inside_context_manager.get(): |
| 301 | + # We are not in context manager, delegate to normal import |
| 302 | + return None |
| 303 | + |
| 304 | + if searching_spec.get(): |
| 305 | + # We are searching for the loader, so we should continue the search |
| 306 | + return None |
| 307 | + |
| 308 | + searching_spec.set(True) |
| 309 | + spec = importlib.util.find_spec(fullname) |
| 310 | + searching_spec.set(False) |
| 311 | + |
| 312 | + if spec is None: |
| 313 | + raise ModuleNotFoundError(f"No module named '{fullname}'") |
| 314 | + |
| 315 | + spec.loader = importlib.util.LazyLoader(spec.loader) |
| 316 | + |
| 317 | + return spec |
| 318 | + |
| 319 | + |
| 320 | +sys.meta_path.insert(0, LazyFinder()) |
| 321 | + |
| 322 | + |
| 323 | +class lazy_imports: |
| 324 | + """ |
| 325 | + Context manager that will block imports and make them lazy. |
| 326 | +
|
| 327 | + >>> import lazy_loader |
| 328 | + >>> with lazy_loader.lazy_imports(): |
| 329 | + >>> from ._mod import some_func |
| 330 | +
|
| 331 | + """ |
| 332 | + |
| 333 | + def __enter__(self): |
| 334 | + # Prevent normal importing |
| 335 | + if inside_context_manager.get(): |
| 336 | + raise ValueError("Nested lazy_imports not allowed.") |
| 337 | + inside_context_manager.set(True) |
| 338 | + return self |
| 339 | + |
| 340 | + def __exit__(self, type, value, tb): |
| 341 | + # Restore normal importing |
| 342 | + inside_context_manager.set(False) |
0 commit comments