The six console scripts (paperspec-check, -build, -validate, -load, -stress, -site) become subcommands of a single `paperspec` command: paperspec validate, load, check, build, stress and site. `python -m paperspec COMMAND` runs the same thing. - paperspec/cli/main.py builds the whole parser, apart from the commands, so `paperspec --help` and `paperspec COMMAND --help` work without a checkout or the tools extra; a command's module is imported only when it runs. - `paperspec` with no command prints the overview: what each command does, examples, and which work anywhere (validate, load) and which need a checkout (check, build, stress, site). Each command's help has a description with advice, and worked examples; a misspelled command gets a suggestion (argparse suggest_on_error); `--version` prints the version and the pinned Unicode version. - Each command module exposes run(args) and returns the exit status. Arguments that parse but do not fit together raise UsageError, which prints the command's usage and exits 2: load now also refuses --built-from or --store without --bundle. - The failure messages say what to do: install paperspec[tools], clone the repository, rerun stress with the printed seed, or run paperspec build when the rebuild check differs. check and stress say how many failures they did not list. - check runs the rebuild as `python -m paperspec build`. - pyproject installs one script, `paperspec`; the pixi tasks and the docs use the subcommands. pixi run check passes (run here as `python -m paperspec check` with src/ on the path); the rebuild is byte-identical. pixi.lock still needs `pixi lock`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
77 lines
2.9 KiB
Python
Executable File
77 lines
2.9 KiB
Python
Executable File
"""Validate packages, or run the correction gate on proposals against a package.
|
|
|
|
Non-normative tooling.
|
|
Usage: paperspec validate PACKAGE [PACKAGE ...]
|
|
paperspec validate --correction PROPOSALS PACKAGE
|
|
A package is a folder, or a zipped package (`.sct.zip`), which is unpacked under the rules of spec/12 §12.2
|
|
before it is checked. Each is checked as spec/22 §22.2 describes, and each failure printed as
|
|
`package: where: rule: message`.
|
|
With --correction, PROPOSALS is a JSON file holding one proposed correction, or a JSON Lines file of several
|
|
applied in order. Each runs through the gate (spec/11 §11.4) against the package's current state, and the record
|
|
it would append to corrections.jsonl is printed, until one is refused. Nothing is written.
|
|
Exit status: 0 when everything is valid, 1 when something is refused, 2 for a usage error.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
|
|
from paperspec.cli import UsageError, require
|
|
|
|
require("cryptography", "jsonschema")
|
|
|
|
from paperspec import corrections, jcs, package, resources # noqa: E402
|
|
from paperspec.schema import Store # noqa: E402
|
|
from paperspec.tree import Refusal # noqa: E402
|
|
|
|
|
|
def read_proposals(path):
|
|
with open(path, encoding="utf-8") as f:
|
|
text = f.read()
|
|
try:
|
|
return [json.loads(text)]
|
|
except json.JSONDecodeError:
|
|
return [json.loads(ln) for ln in text.splitlines() if ln.strip()]
|
|
|
|
|
|
def validate_one(path, store, limit):
|
|
"""The validation report of a package folder or a zipped package."""
|
|
if package.is_zip(path):
|
|
return package.validate_zip(path, store, limit=limit)
|
|
return package.validate(path, store)
|
|
|
|
|
|
def run(a):
|
|
if a.correction and len(a.packages) != 1:
|
|
raise UsageError("--correction checks proposals against exactly one package")
|
|
store, bad = Store(resources.path("schemas")), False
|
|
for p in a.packages:
|
|
for e in validate_one(p, store, a.max_size).errors:
|
|
print("%s: %s: %s: %s" % (p, e["where"], e["rule"], e["message"]), file=sys.stderr)
|
|
bad = True
|
|
if bad:
|
|
return 1
|
|
if not a.correction:
|
|
for p in a.packages:
|
|
print("%s: valid" % p)
|
|
return 0
|
|
pkg = a.packages[0]
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
if package.is_zip(pkg):
|
|
pkg = package.read_zip(pkg, tmp, a.max_size)
|
|
state = package.read_state(pkg)
|
|
if any(n.get("withheld") for n in state.nodes):
|
|
print("%s: doc.policy.withheld: a package that withholds its text takes no corrections" % a.packages[0],
|
|
file=sys.stderr)
|
|
return 1
|
|
for i, proposal in enumerate(read_proposals(a.correction), 1):
|
|
try:
|
|
state, record = corrections.gate(state, proposal, store)
|
|
except Refusal as e:
|
|
where = "%s:%d" % (a.correction, i) + (" " + e.node if e.node else "")
|
|
print("%s: %s: %s" % (where, e.rule, e.message), file=sys.stderr)
|
|
return 1
|
|
print(jcs.dumps(record))
|
|
return 0
|