80 lines
2.5 KiB
Python
80 lines
2.5 KiB
Python
"""
|
|
What every command does the same way — written once.
|
|
|
|
Each old `cli_*` file re-implemented these by hand: read a JSON file and say so
|
|
if it cannot, validate it and print the first five problems, load a style and
|
|
report a bad name, write to a file or to stdout. Eight copies had already begun
|
|
to differ in their wording. Now a command raises `Abort` and `cli.main` turns it
|
|
into the one exit path: `Error: <message>` on stderr, exit 1.
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# The package name is derived, not written: the folder can be copied anywhere
|
|
# and renamed, and help text saying `docgen` for a folder called `docs2` lies.
|
|
ROOT = __package__.rsplit(".", 1)[0]
|
|
PROG = f"python3 -m {ROOT}"
|
|
|
|
|
|
class Abort(Exception):
|
|
"""An expected failure. Printed as one line, exit 1 — never a traceback."""
|
|
|
|
|
|
def read_json(path: Path) -> dict:
|
|
try:
|
|
return json.loads(Path(path).read_text())
|
|
except (OSError, json.JSONDecodeError) as e:
|
|
raise Abort(f"could not read {path}: {e}") from None
|
|
|
|
|
|
def read_ir(path: Path) -> dict:
|
|
"""Read an IR and validate it at the boundary, or abort saying why."""
|
|
from ..ir import check
|
|
|
|
data = read_json(path)
|
|
problems = check(data)
|
|
if problems:
|
|
shown = "\n ".join(problems[:5])
|
|
more = f"\n … and {len(problems) - 5} more" if len(problems) > 5 else ""
|
|
raise Abort(f"{path} is not a valid IR ({len(problems)} problem(s)):\n {shown}{more}")
|
|
return data
|
|
|
|
|
|
def load_style(name: str, theme: str | None):
|
|
from ..style import Style, StyleError
|
|
|
|
try:
|
|
return Style.load(name, theme=theme)
|
|
except StyleError as e:
|
|
raise Abort(str(e)) from None
|
|
|
|
|
|
def style_args(p) -> None:
|
|
p.add_argument("--style", default="lucid", help="A shipped style, or a path to one.")
|
|
p.add_argument("--theme", default=None, help="dark (default) or lucid.")
|
|
|
|
|
|
def emit(text, output: Path | None, report: str = "") -> None:
|
|
"""Write to `output`, or to stdout when there is none."""
|
|
if output is None:
|
|
if isinstance(text, bytes):
|
|
sys.stdout.buffer.write(text)
|
|
else:
|
|
sys.stdout.write(text)
|
|
return
|
|
output = Path(output)
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
if isinstance(text, bytes):
|
|
output.write_bytes(text)
|
|
else:
|
|
output.write_text(text)
|
|
if report:
|
|
print(report)
|
|
|
|
|
|
def dump_ir(ir, output: Path | None, report: str = "") -> None:
|
|
data = ir.to_dict() if hasattr(ir, "to_dict") else ir
|
|
emit(json.dumps(data, indent=2) + "\n", output, report)
|