Skip to content

symfonic.cli.import_scan

import_scan

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