Skip to content

Commit 464728f

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 464728f

File tree

2 files changed

+74
-1
lines changed

2 files changed

+74
-1
lines changed

Diff for: lazy_loader/__init__.py

+45-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,39 @@ 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+
336+
303337

304338
def attach_stub(package_name: str, filename: str):
305339
"""Attach lazily loaded submodules, functions from a type stub.
@@ -308,6 +342,10 @@ def attach_stub(package_name: str, filename: str):
308342
infer ``submodules`` and ``submod_attrs``. This allows static type checkers
309343
to find imports, while still providing lazy loading at runtime.
310344
345+
If the stub file defines `__all__`, it must contain a simple list of
346+
non-empty strings. In this case, the content of `__dir__()` may be
347+
intentionally different from `__all__`.
348+
311349
Parameters
312350
----------
313351
package_name : str
@@ -339,4 +377,10 @@ def attach_stub(package_name: str, filename: str):
339377

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

Diff for: 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)