Skip to content

Commit a17d89e

Browse files
committed
Lazy imports in context manager.
A different (easier/safer) approach to lazy importing using a meta import hook.
1 parent 3dcf52f commit a17d89e

File tree

7 files changed

+89
-2
lines changed

7 files changed

+89
-2
lines changed

lazy_loader/__init__.py

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,30 @@
55
Makes it easy to load subpackages and functions on demand.
66
"""
77
import ast
8+
import builtins
89
import importlib
910
import importlib.util
1011
import inspect
1112
import os
1213
import sys
1314
import types
1415
import warnings
16+
from contextvars import ContextVar
17+
from importlib.abc import MetaPathFinder
18+
from importlib.machinery import ModuleSpec
19+
from typing import Sequence
1520

1621
__all__ = ["attach", "load", "attach_stub"]
1722

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+
1832

1933
def attach(package_name, submodules=None, submod_attrs=None):
2034
"""Attach lazily loaded submodules, functions, or other attributes.
@@ -257,15 +271,72 @@ def attach_stub(package_name: str, filename: str):
257271
incorrectly (e.g. if it contains an relative import from outside of the module)
258272
"""
259273
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"
261276
)
262277

263278
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+
)
265282

266283
with open(stubfile) as f:
267284
stub_node = ast.parse(f.read())
268285

269286
visitor = _StubVisitor()
270287
visitor.visit(stub_node)
271288
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)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import lazy_loader as lazy
2+
3+
with lazy.lazy_imports():
4+
from .some_func import some_func
5+
from . import some_mod, nested_pkg
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import lazy_loader as lazy
2+
3+
from . import nested_mod_eager
4+
5+
with lazy.lazy_imports():
6+
from . import nested_mod_lazy

lazy_loader/tests/fake_pkg_magic/nested_pkg/nested_mod_eager.py

Whitespace-only changes.

lazy_loader/tests/fake_pkg_magic/nested_pkg/nested_mod_lazy.py

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
def some_func():
2+
"""Function with same name as submodule."""
3+
pass
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class SomeClass:
2+
pass

0 commit comments

Comments
 (0)