Source code for jitxlib.parts.export

"""
The parts-check exporter
========================

Runs a design under the `check` lock mode and reports what it found, from the
command line::

    jitx design export parts-check my_board.MyDesign
    jitx design export parts-check my_board.MyDesign --output parts-check.txt
    jitx design export parts-check my_board.MyDesign --fail differs

The report goes to stdout unless `--output` names a file. The export succeeds
whatever the report says, unless `--fail` names categories to fail on.
"""

# No `from __future__ import annotations`: the jitx CLI builds the command's
# options from the unevaluated signature of `export`.

import sys
from enum import StrEnum
from pathlib import Path

from jitx.inspect import extract
from jitx.plugin import Export
from jitx.plugin.export import ExportError
from jitx.run import RuntimeDesign

from .components import DeferredPart
from .lock import LockMode, force_mode
from .resolver import Verdict, name_of


[docs] class FailOn(StrEnum): """A report category that fails the export. `all` is every failing category.""" ALL = "all" UNLOCKED = "unlocked" STALE_CHAIN = "stale-chain" REPLAY_FAILED = "replay-failed" UNRESOLVABLE = "unresolvable" DIFFERS = "differs"
[docs] @Export.register("parts-check") class PartsCheck(Export): """Check the design's parts against a clean resolution and report differences. The report is written to stdout, or to the file named by --output. The export fails only when the report has an entry in a category named by --fail, which may be repeated; --fail all fails on any of them. """
[docs] def preflight( self, output: Path | None = None, fail: list[FailOn] | None = None ) -> None: if output is not None and not output.parent.is_dir(): raise ExportError(f"cannot write {output}: no such directory") force_mode(LockMode.CHECK)
[docs] def export( self, design: RuntimeDesign, /, output: Path | None = None, fail: list[FailOn] | None = None, ) -> None: try: verdict = design_verdict(design) finally: force_mode(None) report = f"Parts check: {design.name}\n{verdict.report()}\n" if output is None: sys.stdout.write(report) sys.stdout.flush() else: output.write_text(report) failed = failing_categories(verdict, fail or []) if failed: raise ExportError( f"parts check failed on {', '.join(failed)}: {verdict.summary()}" )
[docs] def failing_categories(verdict: Verdict, fail: list[FailOn]) -> list[str]: """The categories named by `fail` that `verdict` has entries in, in report order.""" present = [name_of(e.kind, verdict.mode) for e in verdict.failures] wanted = {str(f) for f in fail} return [ name for name in dict.fromkeys(present) if FailOn.ALL in wanted or name in wanted ]
[docs] def design_verdict(design: RuntimeDesign) -> Verdict: """The parts verdict of the run that elaborated `design`. A design with no deferred parts has an empty verdict. """ # Found through the design's own parts rather than `parts_verdict()`: the # runtime elaborates on its bridge thread, and the resolver's run is # thread-local. One design elaborates in one run, so any part will do. for part in extract(design.root, DeferredPart): if part._parts is not None: return part._parts.batch.run.verdict return Verdict(mode=LockMode.CHECK)