We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Modules, Packaging and Tooling step 7 of 41
Work Out What a Module Exports
Write a tool that reads a module's source and reports which names it exports,
using the rules from the article. It builds on
List a Module's Functions with ast.
Parse with ast.parse, loop over module.body, and check each node's type
with isinstance.
Verdict = tuple[Literal["all", "inferred", "dynamic"], frozenset[str]]
def public_surface(source: str) -> Verdict: ...
public_surface("from .core import Thing as Thing\nVERSION = 1\n")
# ("inferred", frozenset({"Thing", "VERSION"}))
public_surface('__all__ = ["missing", "_private"]\n')
# ("all", frozenset({"missing", "_private"}))
public_surface('__all__ = ["a"] + ["b"]\n')
# ("dynamic", frozenset())
Look only at the statements directly in module.body. Read the source
without running it or following its imports. Skip the inside of functions,
classes, if blocks, loops, try blocks and every other block.
First, look for __all__
Find the first statement in module.body that assigns a value to the plain
name __all__. It can be an ast.Assign, including a chained one such as
__all__ = names = [...], or an ast.AnnAssign with a value, such as
__all__: list[str] = []. Skip an annotation without a value.
- If the value is a list or tuple of string constants, return
("all", frozenset(those_strings)). Keep names that start with_and names the module never defines. Duplicates collapse into the set. - Otherwise return
("dynamic", frozenset()).
Stop at that first __all__, and ignore any later change to it. Skip an
unpacking target such as __all__, x = ... and an augmented assignment such
as __all__ += [...].
With no __all__, collect names
Return ("inferred", frozenset(names)), with the names this table gives:
Statement in module.body |
Names to collect |
|---|---|
import X as X or from M import X as X |
X, when the alias is exactly the imported name |
| Any other import, including a renamed or star import | None |
def, async def or class |
The name, unless it starts with _ |
| Assignment to a plain name | That name, unless it starts with _ |
| Annotated assignment to a plain name, with a value | That name, unless it starts with _ |
| Annotation without a value | None |
| Any other statement | None |
- In a chained assignment, check each target. Skip tuple, list, attribute and subscript targets.
- The
_rule applies to definitions and assignments only.import _x as _xstill counts. import os.path as pathadds nothing, because the aliaspathdiffers from the imported nameos.path.- Collect every name you find. Keep a name even if a later statement deletes or reassigns it.
Your solution must pass mypy --strict. Narrow each AST node with
isinstance before you read its fields. Check that each constant is a str
before you add it to a set[str].
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Python reference solution
Sign in to track your attempts toward unlocking the reference solution.