Skip to content

Commit 9579004

Browse files
committed
Let stub-defined __all__ override imports
This enables the use case where objects should be available for lazy-loading while not advertising them. This might be useful for deprecations.
1 parent 4e78314 commit 9579004

File tree

2 files changed

+73
-1
lines changed

2 files changed

+73
-1
lines changed

lazy_loader/__init__.py

+44-1
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ class _StubVisitor(ast.NodeVisitor):
282282
def __init__(self):
283283
self._submodules = set()
284284
self._submod_attrs = {}
285+
self._all = None
285286

286287
def visit_ImportFrom(self, node: ast.ImportFrom):
287288
if node.level != 1:
@@ -300,6 +301,38 @@ def visit_ImportFrom(self, node: ast.ImportFrom):
300301
else:
301302
self._submodules.update(alias.name for alias in node.names)
302303

304+
def visit_Assign(self, node: ast.Assign):
305+
assigned_list = None
306+
for name in node.targets:
307+
if name.id == "__all__":
308+
assigned_list = node.value
309+
310+
if assigned_list is None:
311+
return # early
312+
elif not isinstance(assigned_list, ast.List):
313+
msg = (
314+
f"expected a list assigned to `__all__`, found {type(assigned_list)!r}"
315+
)
316+
raise ValueError(msg)
317+
318+
if self._all is not None:
319+
msg = "expected only one definition of `__all__` in stub"
320+
raise ValueError(msg)
321+
self._all = set()
322+
323+
for constant in assigned_list.elts:
324+
if (
325+
not isinstance(constant, ast.Constant)
326+
or not isinstance(constant.value, str)
327+
or assigned_list == ""
328+
):
329+
msg = (
330+
"expected `__all__` to contain only non-empty strings, "
331+
f"got {constant!r}"
332+
)
333+
raise ValueError(msg)
334+
self._all.add(constant.value)
335+
303336

304337
def attach_stub(package_name: str, filename: str):
305338
"""Attach lazily loaded submodules, functions from a type stub.
@@ -308,6 +341,10 @@ def attach_stub(package_name: str, filename: str):
308341
infer ``submodules`` and ``submod_attrs``. This allows static type checkers
309342
to find imports, while still providing lazy loading at runtime.
310343
344+
If the stub file defines `__all__`, it must contain a simple list of
345+
non-empty strings. In this case, the content of `__dir__()` may be
346+
intentionally different from `__all__`.
347+
311348
Parameters
312349
----------
313350
package_name : str
@@ -339,4 +376,10 @@ def attach_stub(package_name: str, filename: str):
339376

340377
visitor = _StubVisitor()
341378
visitor.visit(stub_node)
342-
return attach(package_name, visitor._submodules, visitor._submod_attrs)
379+
380+
__getattr__, __dir__, __all__ = attach(
381+
package_name, visitor._submodules, visitor._submod_attrs
382+
)
383+
if visitor._all is not None:
384+
__all__ = visitor._all
385+
return __getattr__, __dir__, __all__

lazy_loader/tests/test_lazy_loader.py

+29
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,35 @@ def test_stub_loading_parity():
143143
assert stub_getter("some_func") == fake_pkg.some_func
144144

145145

146+
FAKE_STUB_OVERRIDE_ALL = """
147+
__all__ = [
148+
"rank",
149+
"gaussian",
150+
"sobel",
151+
"scharr",
152+
"roberts",
153+
# `prewitt` not included!
154+
"__version__", # included but not imported in stub
155+
]
156+
157+
from . import rank
158+
from ._gaussian import gaussian
159+
from .edges import sobel, scharr, prewitt, roberts
160+
"""
161+
162+
163+
def test_stub_override_all(tmp_path):
164+
stub = tmp_path / "stub.pyi"
165+
stub.write_text(FAKE_STUB_OVERRIDE_ALL)
166+
_get, _dir, _all = lazy.attach_stub("my_module", str(stub))
167+
168+
expect_dir = {"gaussian", "sobel", "scharr", "prewitt", "roberts", "rank"}
169+
assert set(_dir()) == expect_dir
170+
171+
expect_all = {"rank", "gaussian", "sobel", "scharr", "roberts", "__version__"}
172+
assert set(_all) == expect_all
173+
174+
146175
def test_stub_loading_errors(tmp_path):
147176
stub = tmp_path / "stub.pyi"
148177
stub.write_text("from ..mod import func\n")

0 commit comments

Comments
 (0)