Guide 05 of 9
Catalogues and builders
Name the programs an application may run, and wrap argument construction in small, validated builders.
Define an application catalogue
A catalogue names the programs an application may build commands for.
Give them a project, a ProjectSettings, and every
command carries the project's name, documentation locations, and noise
rules. ProgramCatalogue.from_project()
builds a catalogue from one project; ProgramCatalogue(projects=(...))
holds several, each owning its programs. Cuprum stores noise rules for
log processing downstream; it does not filter output itself.
import sys
from cuprum import Program, ProgramCatalogue, ProjectSettings, sh
from cuprum.catalogue import DuplicateProgramError
PYTHON = Program(sys.executable)
settings = ProjectSettings(
name="report-builder",
programs=(PYTHON,),
documentation_locations=("docs/reports.md",),
noise_rules=(r"^progress:",),
)
catalogue = ProgramCatalogue.from_project(settings)
command = sh.make(PYTHON, catalogue=catalogue)("-c", "print('ok')")
assert command.project.name == "report-builder"
assert command.project.noise_rules == (r"^progress:",)
assert catalogue.lookup(PYTHON).project.documentation_locations == ("docs/reports.md",)
try:
ProgramCatalogue.from_programs(PYTHON, PYTHON)
except DuplicateProgramError as exc:
assert exc.program == PYTHON
else:
raise AssertionError("a program listed twice should be rejected")
A catalogue that lists the same program twice raises
DuplicateProgramError, and two projects with one name raise
DuplicateProjectError; both are ValueError
subclasses from cuprum.catalogue. An absolute path is
allowlisted exactly as written: /usr/bin/git does not also
permit git found on PATH.
catalogue.visible_settings maps each project name to its
settings for services that pass this metadata on.
Wrap commands in project builders
Put argument construction in small functions that validate their input
and return a SafeCmd. Callers then cannot build an
unvalidated command line. cuprum.builders
provides ready-made builders for Git, rsync, and tar, and the
safe_path() and git_ref() validators they use.
They use the default catalogue, so building their commands does not
need the tools installed; running them does.
import sys
from pathlib import Path
from cuprum import Program, ProgramCatalogue, SafeCmd, sh
from cuprum.builders import git_rev_parse, safe_path
PYTHON = Program(sys.executable)
CATALOGUE = ProgramCatalogue.from_programs(PYTHON, name="line-counter")
def count_lines(path: Path) -> SafeCmd:
"""Build a command that counts the lines in one existing file."""
checked = safe_path(path.resolve())
script = "import sys; print(sum(1 for _ in open(sys.argv[1])))"
return sh.make(PYTHON, catalogue=CATALOGUE)("-c", script, str(checked))
result = count_lines(Path("README.md")).run_sync()
assert result.ok and int(result.stdout) > 0
command = git_rev_parse("main")
assert command.argv_with_program == ("git", "rev-parse", "main")
try:
git_rev_parse("main..evil")
except ValueError:
pass
else:
raise AssertionError("an unsafe ref should be rejected")
safe_path() rejects empty paths, NUL characters, and
.. segments, and requires an absolute path unless
allow_relative=True. git_ref() rejects
whitespace, a leading -, .., @{,
and other constructs Git would read as options or revision syntax.