52 lines
2 KiB
Python
52 lines
2 KiB
Python
"""Generate exact repository source anchors without importing application code."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
import json
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
EXAMPLES_DIR = ROOT / "src" / "gallery" / "examples"
|
|
OUTPUT = ROOT / "src" / "gallery" / "source_manifest.json"
|
|
|
|
|
|
def collect() -> dict[str, dict[str, str | int]]:
|
|
manifest: dict[str, dict[str, str | int]] = {}
|
|
for path in sorted(EXAMPLES_DIR.glob("*.py")):
|
|
if path.name == "__init__.py":
|
|
continue
|
|
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
|
functions = {
|
|
node.name: node.lineno
|
|
for node in tree.body
|
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
}
|
|
for node in ast.walk(tree):
|
|
if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Name):
|
|
continue
|
|
if node.func.id != "Example" or len(node.args) < 5:
|
|
continue
|
|
example_id, builder = node.args[0], node.args[4]
|
|
if not isinstance(example_id, ast.Constant) or not isinstance(example_id.value, str):
|
|
raise TypeError(f"Example id must be a string in {path}")
|
|
if not isinstance(builder, ast.Name) or builder.id not in functions:
|
|
raise ValueError(f"Example {example_id.value} must reference a local function")
|
|
if example_id.value in manifest:
|
|
raise ValueError(f"Duplicate example id: {example_id.value}")
|
|
manifest[example_id.value] = {
|
|
"path": path.relative_to(ROOT).as_posix(),
|
|
"line": functions[builder.id],
|
|
"function": builder.id,
|
|
}
|
|
return manifest
|
|
|
|
|
|
def main() -> None:
|
|
content = json.dumps(collect(), ensure_ascii=False, indent=2, sort_keys=True) + "\n"
|
|
OUTPUT.write_text(content, encoding="utf-8")
|
|
print(f"Generated {OUTPUT.relative_to(ROOT)}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|