|
4 | 4 |
|
5 | 5 | Makes it easy to load subpackages and functions on demand. |
6 | 6 | """ |
| 7 | +import ast |
7 | 8 | import importlib |
8 | 9 | import importlib.util |
9 | 10 | import inspect |
10 | 11 | import os |
11 | 12 | import sys |
12 | 13 | import types |
13 | 14 |
|
14 | | -__all__ = ["attach", "load"] |
| 15 | +__all__ = ["attach", "load", "attach_stub"] |
15 | 16 |
|
16 | 17 |
|
17 | 18 | def attach(package_name, submodules=None, submod_attrs=None): |
@@ -189,3 +190,61 @@ def myfunc(): |
189 | 190 | loader.exec_module(module) |
190 | 191 |
|
191 | 192 | return module |
| 193 | + |
| 194 | + |
| 195 | +class _StubVisitor(ast.NodeVisitor): |
| 196 | + """AST visitor to parse a stub file for submodules and submod_attrs.""" |
| 197 | + |
| 198 | + def __init__(self): |
| 199 | + self._submodules = set() |
| 200 | + self._submod_attrs = {} |
| 201 | + |
| 202 | + def visit_ImportFrom(self, node: ast.ImportFrom): |
| 203 | + if node.level != 1: |
| 204 | + raise ValueError( |
| 205 | + "Only within-module imports are supported (`from .* import`)" |
| 206 | + ) |
| 207 | + if node.module: |
| 208 | + attrs: list = self._submod_attrs.setdefault(node.module, []) |
| 209 | + attrs.extend(alias.name for alias in node.names) |
| 210 | + else: |
| 211 | + self._submodules.update(alias.name for alias in node.names) |
| 212 | + |
| 213 | + |
| 214 | +def attach_stub(package_name: str, filename: str): |
| 215 | + """Attach lazily loaded submodules, functions from a type stub. |
| 216 | +
|
| 217 | + This is a variant on ``attach`` that will parse a `.pyi` stub file to |
| 218 | + infer ``submodules`` and ``submod_attrs``. This allows static type checkers |
| 219 | + to find imports, while still providing lazy loading at runtime. |
| 220 | +
|
| 221 | + Parameters |
| 222 | + ---------- |
| 223 | + package_name : str |
| 224 | + Typically use ``__name__``. |
| 225 | + filename : str |
| 226 | + Path to `.py` file which has an adjacent `.pyi` file. |
| 227 | + Typically use ``__file__``. |
| 228 | +
|
| 229 | + Returns |
| 230 | + ------- |
| 231 | + __getattr__, __dir__, __all__ |
| 232 | + The same output as ``attach``. |
| 233 | +
|
| 234 | + Raises |
| 235 | + ------ |
| 236 | + ValueError |
| 237 | + If a stub file is not found for `filename`, or if the stubfile is formmated |
| 238 | + incorrectly (e.g. if it contains an relative import from outside of the module) |
| 239 | + """ |
| 240 | + stubfile = filename if filename.endswith("i") else f"{filename}i" |
| 241 | + |
| 242 | + if not os.path.exists(stubfile): |
| 243 | + raise ValueError(f"Cannot load imports from non-existent stub {stubfile!r}") |
| 244 | + |
| 245 | + with open(stubfile) as f: |
| 246 | + stub_node = ast.parse(f.read()) |
| 247 | + |
| 248 | + visitor = _StubVisitor() |
| 249 | + visitor.visit(stub_node) |
| 250 | + return attach(package_name, visitor._submodules, visitor._submod_attrs) |
0 commit comments