Find the symfonic imports a scaffolded project will contain (T4.3.1).
Separate from :mod:symfonic.cli.public_api because reading imports and
judging them are different jobs: this module knows nothing about what is
public, and the registry knows nothing about Jinja.
Why a regex and not :mod:ast: most of the inputs are .j2 templates,
which are not parseable Python. Parsing the rendered output instead would
check exactly one component selection and quietly miss every {% if %}
branch — and the branches are where the interesting imports live (the
Postgres seeder, the store domain, the metrics wiring).
ImportRef
dataclass
ImportRef(module: str, name: str | None, source: str, line: int)
One imported name, located precisely enough to fix.
name is None for a bare import symfonic.x, which binds the
package rather than a symbol.
scan_symfonic_imports
scan_symfonic_imports(text: str, source: str) -> list[ImportRef]
Return every symfonic import in text, one entry per bound name.
The parenthesised multi-line form is consumed to its closing paren, which
is the shape the scaffold's observability imports take; missing it would
silently under-report exactly the imports most worth checking.
Source code in src/symfonic/cli/import_scan.py
| def scan_symfonic_imports(text: str, source: str) -> list[ImportRef]:
"""Return every ``symfonic`` import in *text*, one entry per bound name.
The parenthesised multi-line form is consumed to its closing paren, which
is the shape the scaffold's observability imports take; missing it would
silently under-report exactly the imports most worth checking.
"""
refs: list[ImportRef] = []
lines = text.splitlines()
index = 0
while index < len(lines):
line = lines[index]
plain = _IMPORT.match(line)
if plain is not None:
refs.append(ImportRef(plain.group(1), None, source, index + 1))
index += 1
continue
match = _FROM.match(line)
if match is None:
index += 1
continue
module, tail = match.group(1), match.group(2)
start = index + 1
if tail.strip().startswith("("):
body = [tail.strip()[1:]]
while ")" not in body[-1] and index + 1 < len(lines):
index += 1
body.append(lines[index])
tail = " ".join(body).replace(")", "")
refs.extend(ImportRef(module, name, source, start) for name in _names(tail))
index += 1
return refs
|
template_import_refs
template_import_refs(templates_root: Path) -> list[ImportRef]
Scan every scaffold template for symfonic imports.
Both .j2 templates and the plain .py files copied verbatim are
read: the scaffolder emits both, and an import is equally binding either
way.
Source code in src/symfonic/cli/import_scan.py
| def template_import_refs(templates_root: Path) -> list[ImportRef]:
"""Scan every scaffold template for ``symfonic`` imports.
Both ``.j2`` templates and the plain ``.py`` files copied verbatim are
read: the scaffolder emits both, and an import is equally binding either
way.
"""
refs: list[ImportRef] = []
for path in sorted(templates_root.rglob("*")):
if not path.is_file() or "__pycache__" in path.parts:
continue
if path.suffix not in {".j2", ".py"}:
continue
rel = path.relative_to(templates_root).as_posix()
refs.extend(scan_symfonic_imports(path.read_text(encoding="utf-8"), rel))
return refs
|