Files
soleprint/soleprint/atlas2/docgen/cli/common.py

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)