|
6 | 6 | """
|
7 | 7 | from __future__ import annotations
|
8 | 8 |
|
9 |
| -import ast |
10 |
| -import importlib |
11 |
| -import importlib.util |
12 |
| -import inspect |
13 |
| -import os |
14 | 9 | import sys
|
15 |
| -import types |
16 | 10 | from contextlib import contextmanager
|
17 | 11 | from importlib.util import LazyLoader
|
18 | 12 |
|
19 |
| -__all__ = ["attach", "load", "attach_stub", "lazy_loader"] |
20 |
| - |
21 |
| - |
22 |
| -def attach(package_name, submodules=None, submod_attrs=None): |
23 |
| - """Attach lazily loaded submodules, functions, or other attributes. |
24 |
| -
|
25 |
| - Typically, modules import submodules and attributes as follows:: |
26 |
| -
|
27 |
| - import mysubmodule |
28 |
| - import anothersubmodule |
29 |
| -
|
30 |
| - from .foo import someattr |
31 |
| -
|
32 |
| - The idea is to replace a package's `__getattr__`, `__dir__`, and |
33 |
| - `__all__`, such that all imports work exactly the way they would |
34 |
| - with normal imports, except that the import occurs upon first use. |
35 |
| -
|
36 |
| - The typical way to call this function, replacing the above imports, is:: |
37 |
| -
|
38 |
| - __getattr__, __dir__, __all__ = lazy.attach( |
39 |
| - __name__, |
40 |
| - ['mysubmodule', 'anothersubmodule'], |
41 |
| - {'foo': ['someattr']} |
42 |
| - ) |
43 |
| -
|
44 |
| - This functionality requires Python 3.7 or higher. |
45 |
| -
|
46 |
| - Parameters |
47 |
| - ---------- |
48 |
| - package_name : str |
49 |
| - Typically use ``__name__``. |
50 |
| - submodules : set |
51 |
| - List of submodules to attach. |
52 |
| - submod_attrs : dict |
53 |
| - Dictionary of submodule -> list of attributes / functions. |
54 |
| - These attributes are imported as they are used. |
55 |
| -
|
56 |
| - Returns |
57 |
| - ------- |
58 |
| - __getattr__, __dir__, __all__ |
59 |
| -
|
60 |
| - """ |
61 |
| - if submod_attrs is None: |
62 |
| - submod_attrs = {} |
63 |
| - |
64 |
| - if submodules is None: |
65 |
| - submodules = set() |
66 |
| - else: |
67 |
| - submodules = set(submodules) |
68 |
| - |
69 |
| - attr_to_modules = { |
70 |
| - attr: mod for mod, attrs in submod_attrs.items() for attr in attrs |
71 |
| - } |
72 |
| - |
73 |
| - __all__ = list(submodules | attr_to_modules.keys()) |
74 |
| - |
75 |
| - def __getattr__(name): |
76 |
| - if name in submodules: |
77 |
| - return importlib.import_module(f"{package_name}.{name}") |
78 |
| - elif name in attr_to_modules: |
79 |
| - submod_path = f"{package_name}.{attr_to_modules[name]}" |
80 |
| - submod = importlib.import_module(submod_path) |
81 |
| - attr = getattr(submod, name) |
82 |
| - |
83 |
| - # If the attribute lives in a file (module) with the same |
84 |
| - # name as the attribute, ensure that the attribute and *not* |
85 |
| - # the module is accessible on the package. |
86 |
| - if name == attr_to_modules[name]: |
87 |
| - pkg = sys.modules[package_name] |
88 |
| - pkg.__dict__[name] = attr |
89 |
| - |
90 |
| - return attr |
91 |
| - else: |
92 |
| - raise AttributeError(f"No {package_name} attribute {name}") |
93 |
| - |
94 |
| - def __dir__(): |
95 |
| - return __all__ |
96 |
| - |
97 |
| - if os.environ.get("EAGER_IMPORT", ""): |
98 |
| - for attr in set(attr_to_modules.keys()) | submodules: |
99 |
| - __getattr__(attr) |
100 |
| - |
101 |
| - return __getattr__, __dir__, list(__all__) |
102 |
| - |
103 |
| - |
104 |
| -class DelayedImportErrorModule(types.ModuleType): |
105 |
| - def __init__(self, frame_data, *args, **kwargs): |
106 |
| - self.__frame_data = frame_data |
107 |
| - super().__init__(*args, **kwargs) |
108 |
| - |
109 |
| - def __getattr__(self, x): |
110 |
| - if x in ("__class__", "__file__", "__frame_data"): |
111 |
| - super().__getattr__(x) |
112 |
| - else: |
113 |
| - fd = self.__frame_data |
114 |
| - raise ModuleNotFoundError( |
115 |
| - f"No module named '{fd['spec']}'\n\n" |
116 |
| - "This error is lazily reported, having originally occured in\n" |
117 |
| - f' File {fd["filename"]}, line {fd["lineno"]}, in {fd["function"]}\n\n' |
118 |
| - f'----> {"".join(fd["code_context"]).strip()}' |
119 |
| - ) |
120 |
| - |
121 |
| - |
122 |
| -def load(fullname, error_on_import=False): |
123 |
| - """Return a lazily imported proxy for a module. |
124 |
| -
|
125 |
| - We often see the following pattern:: |
126 |
| -
|
127 |
| - def myfunc(): |
128 |
| - from numpy import linalg as la |
129 |
| - la.norm(...) |
130 |
| - .... |
131 |
| -
|
132 |
| - This is to prevent a module, in this case `numpy`, from being |
133 |
| - imported at function definition time, since that can be slow. |
134 |
| -
|
135 |
| - This function provides a proxy module that, upon access, imports |
136 |
| - the actual module. So the idiom equivalent to the above example is:: |
137 |
| -
|
138 |
| - la = lazy.load("numpy.linalg") |
139 |
| -
|
140 |
| - def myfunc(): |
141 |
| - la.norm(...) |
142 |
| - .... |
143 |
| -
|
144 |
| - The initial import time is fast because the actual import is delayed |
145 |
| - until the first attribute is requested. The overall import time may |
146 |
| - decrease as well for users that don't make use of large portions |
147 |
| - of the library. |
148 |
| -
|
149 |
| - Parameters |
150 |
| - ---------- |
151 |
| - fullname : str |
152 |
| - The full name of the module or submodule to import. For example:: |
153 |
| -
|
154 |
| - sp = lazy.load('scipy') # import scipy as sp |
155 |
| - spla = lazy.load('scipy.linalg') # import scipy.linalg as spla |
156 |
| - error_on_import : bool |
157 |
| - Whether to postpone raising import errors until the module is accessed. |
158 |
| - If set to `True`, import errors are raised as soon as `load` is called. |
159 |
| -
|
160 |
| - Returns |
161 |
| - ------- |
162 |
| - pm : importlib.util._LazyModule |
163 |
| - Proxy module. Can be used like any regularly imported module. |
164 |
| - Actual loading of the module occurs upon first attribute request. |
165 |
| -
|
166 |
| - """ |
167 |
| - try: |
168 |
| - return sys.modules[fullname] |
169 |
| - except KeyError: |
170 |
| - pass |
171 |
| - |
172 |
| - spec = importlib.util.find_spec(fullname) |
173 |
| - if spec is None: |
174 |
| - if error_on_import: |
175 |
| - raise ModuleNotFoundError(f"No module named '{fullname}'") |
176 |
| - else: |
177 |
| - try: |
178 |
| - parent = inspect.stack()[1] |
179 |
| - frame_data = { |
180 |
| - "spec": fullname, |
181 |
| - "filename": parent.filename, |
182 |
| - "lineno": parent.lineno, |
183 |
| - "function": parent.function, |
184 |
| - "code_context": parent.code_context, |
185 |
| - } |
186 |
| - return DelayedImportErrorModule(frame_data, "DelayedImportErrorModule") |
187 |
| - finally: |
188 |
| - del parent |
189 |
| - |
190 |
| - module = importlib.util.module_from_spec(spec) |
191 |
| - sys.modules[fullname] = module |
192 |
| - |
193 |
| - loader = importlib.util.LazyLoader(spec.loader) |
194 |
| - loader.exec_module(module) |
195 |
| - |
196 |
| - return module |
197 |
| - |
198 |
| - |
199 |
| -class _StubVisitor(ast.NodeVisitor): |
200 |
| - """AST visitor to parse a stub file for submodules and submod_attrs.""" |
201 |
| - |
202 |
| - def __init__(self): |
203 |
| - self._submodules = set() |
204 |
| - self._submod_attrs = {} |
205 |
| - |
206 |
| - def visit_ImportFrom(self, node: ast.ImportFrom): |
207 |
| - if node.level != 1: |
208 |
| - raise ValueError( |
209 |
| - "Only within-module imports are supported (`from .* import`)" |
210 |
| - ) |
211 |
| - if node.module: |
212 |
| - attrs: list = self._submod_attrs.setdefault(node.module, []) |
213 |
| - attrs.extend(alias.name for alias in node.names) |
214 |
| - else: |
215 |
| - self._submodules.update(alias.name for alias in node.names) |
216 |
| - |
217 |
| - |
218 |
| -def attach_stub(package_name: str, filename: str): |
219 |
| - """Attach lazily loaded submodules, functions from a type stub. |
220 |
| -
|
221 |
| - This is a variant on ``attach`` that will parse a `.pyi` stub file to |
222 |
| - infer ``submodules`` and ``submod_attrs``. This allows static type checkers |
223 |
| - to find imports, while still providing lazy loading at runtime. |
224 |
| -
|
225 |
| - Parameters |
226 |
| - ---------- |
227 |
| - package_name : str |
228 |
| - Typically use ``__name__``. |
229 |
| - filename : str |
230 |
| - Path to `.py` file which has an adjacent `.pyi` file. |
231 |
| - Typically use ``__file__``. |
232 |
| -
|
233 |
| - Returns |
234 |
| - ------- |
235 |
| - __getattr__, __dir__, __all__ |
236 |
| - The same output as ``attach``. |
237 |
| -
|
238 |
| - Raises |
239 |
| - ------ |
240 |
| - ValueError |
241 |
| - If a stub file is not found for `filename`, or if the stubfile is formmated |
242 |
| - incorrectly (e.g. if it contains an relative import from outside of the module) |
243 |
| - """ |
244 |
| - stubfile = filename if filename.endswith("i") else f"{filename}i" |
245 |
| - |
246 |
| - if not os.path.exists(stubfile): |
247 |
| - raise ValueError(f"Cannot load imports from non-existent stub {stubfile!r}") |
248 |
| - |
249 |
| - with open(stubfile) as f: |
250 |
| - stub_node = ast.parse(f.read()) |
251 |
| - |
252 |
| - visitor = _StubVisitor() |
253 |
| - visitor.visit(stub_node) |
254 |
| - return attach(package_name, visitor._submodules, visitor._submod_attrs) |
| 13 | +__all__ = ["lazy_loader"] |
255 | 14 |
|
256 | 15 |
|
257 | 16 | class LazyFinder:
|
|
0 commit comments