API reference¶
Auto-generated from the source via
mkdocstrings. Everything here is importable
straight from the footman package (from footman import task, run, App).
Defining tasks¶
footman.registry.Group
¶
A node in the command tree: named tasks and nested sub-groups.
__call__
¶
Run this group's default action — the imperative mirror of a bare
fm <group> and of pre=[group].
A runnable group (one with an @group.default) is callable from a task
body the way a task is: a check task can call lint(fix=fix). It runs
the default's action synchronously and in order — a custom body as
written, or, for an empty-body default, the group's own tasks, each
handed the arguments it declares (partial reach, by name). Like every
body call it forwards arguments explicitly and runs to completion before
the next statement; prerequisites and parallelism stay the scheduler's
job — reach for a real chain, pre=, or parallel() for those.
default
¶
default(fn: Callable[_P, _R_co]) -> TaskFn[_P, _R_co]
default(
fn: Task | None = None,
*,
pre: Sequence[Task] = (),
post: Sequence[Task] = (),
progress: bool = True,
infinite: bool = False,
confirm: str = "",
interactive: bool = False,
keep_going: bool | None = None,
atomic: bool = False,
) -> Task | Callable[[Task], Task]
Register fn as this group's default action — what a bare
fm <group> runs, and what the group returns when called.
Usable bare (@group.default) or parameterised
(@group.default(keep_going=True)). It takes the same orchestration
options as @task — pre/post, progress, infinite, confirm,
interactive, keep_going, atomic — with no name (the group
already names it).
The function's signature is the group's option surface, so it takes
flags/options only: a positional parameter is rejected at load time,
because a bare word after a group names a child, not a value. Model a
positional action as a task, or take free arguments via -- passthrough.
An empty-body default fans the group's own tasks out in parallel, so
interactive=True on one is rejected — there is no single body to own
the terminal. Give the default a real body to make it interactive.
finalize
¶
Register a hook that edits the discovered command tree in place.
Every @finalize function runs once, after the whole tasks.py cascade
is assembled but before dispatch, handed a Tasks view of the merged
tree. Its edits are part of the plan, never a runtime surprise: an added
pre runs and shows in --dry-run, a disabled task drops from listings
and completion. It is footman's collection_modifyitems.
Finalizers run in cascade order — root's first, the folder nearest your
cwd last, each seeing the previous ones' edits — the same "local overrides
global" precedence the task cascade itself uses. Read and edit each task
through the TaskView surface, never the private _footman_* attributes.
@footman.finalize
def gate_deploys(tasks):
audit = tasks["audit"]
for t in tasks:
if t.name.startswith("deploy"):
t.add_pre(audit)
group
¶
Create and register a nested command group, returning it.
opts
¶
opts(**overrides: Any) -> _Opted
Per-use option overrides for this group's default action, the same
.opts() a task has — pre=[lint.opts(keep_going=True)]. Overrides ride
the group's default when it runs (bare, as a pre=, or called).
task
¶
task(fn: Callable[_P, _R_co]) -> TaskFn[_P, _R_co]
task(
fn: Task | None = None,
*,
name: str = "",
pre: Sequence[Task] = (),
post: Sequence[Task] = (),
progress: bool = True,
infinite: bool = False,
confirm: str = "",
interactive: bool = False,
keep_going: bool | None = None,
atomic: bool = False,
) -> Task | Callable[[Task], Task]
Register a function as a task.
Usable bare (@task) or parameterised (@task(name="build")) to
override the command name. pre/post declare dependency tasks (by
reference) that run before/after this one — the scheduler runs
independent prerequisites in parallel:
Availability gating lives in the @requires decorators — stack
@requires, @requires_dep, @requires_tool, or @requires_env
above @task to list a task as unavailable (with a reason) where it
can't run, rather than hide it. To hide a task entirely, use plain
Python: if sys.platform == "darwin": @task ...
progress=False marks a task whose duration has no rhyme or
reason (a REPL, a watcher, a network fetch): any run containing it
never records timing history and never shows a determinate
progress bar — the indeterminate pulse still does.
infinite=True marks a task that runs until stopped — a dev
server, a follow-mode tail. It implies progress=False, and the
run swaps the status line for a one-time hint that Ctrl-C is how
this ends. Listings and help carry the same note.
confirm="…" gates the task on a yes/no answer asked before the
task and its prerequisites run — deny and the task (and its
subtree) is skipped; --yes auto-answers it. interactive=True
hands the task the real terminal — no output capture, sole stdio —
so its body can prompt or run a REPL; it can't run under --json, and
because it owns the terminal, a run that contains an interactive task
goes fully sequential — that task and everything else, one at a time.
Availability gates¶
Stack these above @task to list a task as unavailable (with a reason) where
it can't run. Every gate is evaluated live, and all failures are collected.
footman.registry.requires
¶
footman.registry.requires_dep
¶
Gate a task on Python modules being importable (find_spec, no import).
Keep the real import in the body; this only checks availability, so a
missing optional dependency lists as a clean reason, never an import crash.
footman.registry.requires_tool
¶
Gate a task on command-line tools being on PATH (shutil.which).
footman.registry.requires_env
¶
Gate a task on environment variables being set (in os.environ).
Running commands¶
footman.context.run
¶
run(
cmd: str | list[str] | Callable[..., Any],
*args: Any,
nofail: bool = False,
silent: bool = False,
capture: bool = True,
title: str | None = None,
env: dict[str, str] | None = None,
cwd: str | Path | None = None,
encoding: str | None = "utf-8",
shell: bool | str = False,
strict: bool = False,
clean: bool = False,
_show: Invocation | None = None,
) -> Result
Run a command or a Python callable in the current task's context.
Subprocess output is decoded as UTF-8 by default; pass encoding= for a
tool that speaks another code page, or encoding=None for the locale
default. Ignored for callables (in-process, no bytes boundary).
_show is an internal channel from the tools.* bridge: a structured
view of the call, so the shown command line can be normalised and
role-coloured while execution runs whatever the tool needs. An explicit
title still wins; a direct run([...]) is unaffected.
footman.context.Result
¶
Bases: int
The outcome of one run() call — and the value run() returns.
A Result is the exit code: it subclasses int, so code = run(...),
if run(...), and run(...) == 0 all keep working. It also carries the
captured output, split by stream, and the command that produced it — so
run("git rev-parse HEAD").stdout.strip() reads the hash without the
stderr noise glued on. stdout/stderr are separated for both subprocess
and in-process runs; a streamed run (capture=False) leaves them empty.
command
instance-attribute
¶
command: str
The command line that ran, normalised for reading — options in
separated form, values shell-quoted. What recording() asserts against,
and what the terminal shows.
output
property
¶
output: str
stdout then stderr, concatenated — a convenience for "show me
everything". NOT interleaved in real time (each stream is captured
whole); when the order across the two streams matters, read stdout
and stderr separately.
raw
instance-attribute
¶
raw: str
The exact command line executed, shell-quoted — the bytes footman
handed the tool, which may spell an option --flag=value where
command shows --flag value. What --verbose prints. Equal to
command when there is nothing to normalise.
footman.context.parallel
¶
Run task calls / thunks concurrently; wait; fail if any fail.
Each call runs in a child of the current context with its own output buffer,
flushed atomically on completion so concurrent output never interleaves.
Pass task functions directly (parallel(lint, typecheck)) or thunks for
arguments (parallel(lambda: build("web"), lambda: build("api"))).
footman.context.passthrough
¶
Arguments after -- on the command line, for the running task.
footman.context.inherited
¶
inherited() -> Any
The task this one shadows in the cascade — footman's super().
A nearer tasks.py overriding a task by name usually wants to extend
it, not replace it. Call this inside the overriding task's body to get
the task it shadows, then call that like the plain function it is:
# svc/api/tasks.py — the root also defines `check`
@task
def check(fix: bool = False, contracts: bool = True):
inherited()(fix=fix) # arguments are forwarded explicitly
if contracts:
run("./verify-contracts.sh")
Forwarding is deliberately manual: the two signatures are independent
(a leaf usually adds a parameter), so automatic forwarding could only
drop arguments silently or fail at run time — where spelling the call
out shows you the mismatch as you type it. Being an ordinary call,
it also runs to completion
before the next statement — and composes with parallel(inherited(),
extra) when you want otherwise.
fm --where <task> lists the whole shadow chain; fm --help <task>
shows the inherited task's options, so you can read the forwarding
call straight off it.
footman.context.progress
¶
Report this task's own progress: done of total units.
Some work knows exactly how far along it is — 23 of 150 migrations, bytes of a download — and that is better evidence than any duration history. A reporting task's counts drive the live bar directly (counted beats estimated), so the bar is honest on the very first run, where the estimator would still be guessing.
@task
def migrate():
for i, record in enumerate(records, 1):
apply(record)
progress(i, len(records))
A total of 0 (or less) clears the report, returning the run to its
estimate. Outside a run, or with no live status line, this is a
no-op — plain calls and captured runs cost nothing.
footman.context.track
¶
Iterate iterable, reporting progress as it goes.
The ergonomic form of progress(): the total comes from len() when
the iterable has one, or from total when you know it for a
generator. Without either, iteration still works — the run simply
keeps whatever progress it had.
Asking the person running it¶
footman.context.prompt
¶
Ask the person running the task for a line of input.
A bare input() doesn't work in a task: its prompt goes to stdout, which
footman buffers per task so parallel output never interleaves (and --json
stays one envelope), so the prompt is swallowed and the task looks hung.
prompt() writes to the real terminal on stderr instead — never captured —
and serialises concurrent prompts.
Usable only inside an @task(interactive=True) task; called in an ordinary
task body it raises a taught error naming the two fixes. Off a terminal,
under --no-input, or when it would otherwise block, it returns default
if given, else raises — an unattended run fails loudly. For a value a
script must supply, take it as a task parameter (a CLI flag) instead.
footman.context.confirm
¶
Ask a yes/no question. --yes auto-answers yes; Enter alone takes
default; off a terminal or under --no-input the answer is default.
Guarded like prompt() — interactive tasks only.
footman.context.select
¶
select(
message: str,
options: Sequence[Any],
*,
multiple: bool = False,
default: Any = _UNSET,
) -> Any
Let the person pick from a runtime-computed list — the one interactive case a flag can't cover, because the options aren't known until the task runs (which changed packages to release, which stale branches to delete).
options are strings, or (label, value) pairs to show one thing and
return another. multiple=True returns the chosen subset as a list;
otherwise one value is returned. Guarded like prompt() (interactive tasks
only), and off a terminal or under --no-input it returns default, or
raises if none was given.
Fetching¶
footman._fetch.fetch
¶
fetch(
url: str,
*,
into: Path | str | None = None,
sha256: str = "",
backend: str = "",
refresh: bool = False,
) -> Path
Download url (cached), returning the path to the local file.
A second call for the same URL revalidates with the server (ETag /
Last-Modified) rather than re-downloading; a 304 Not Modified
costs one round trip and keeps "cached" honest. Pass refresh to
skip revalidation and fetch unconditionally.
@task
def deps():
"Fetch the toolchain."
archive = fetch(TOOLCHAIN_URL, sha256="9f86d0…")
tools.tar("-xzf", archive, "-C", "vendor")
into copies the cached file to a path of your choosing (and returns that path). sha256 verifies what arrived and refuses a mismatch — the way to make a build reproducible. backend overrides the configured one for this call.
Under --dry-run nothing is downloaded: the step is recorded and
the would-be cache path returned, so a plan can be inspected safely.
footman._fetch.FetchError
¶
Bases: Exception
A download failed, or arrived wrong (checksum, missing backend).
footman.context.Context
dataclass
¶
State for one running task: environment, flags, passthrough, output.
assume_yes
class-attribute
instance-attribute
¶
assume_yes: bool = False
--yes: every confirm() gate auto-answers yes, for CI and scripts.
atomic
class-attribute
instance-attribute
¶
atomic: bool = False
@task(atomic=True): this task's subprocesses opt out of fail-fast's
kill — they run to completion so a mid-write can't be truncated.
cwd
class-attribute
instance-attribute
¶
cwd: Path | None = None
Where run() executes: the folder that defined the task; None
means the process cwd (plain calls outside a footman run).
dry_run
class-attribute
instance-attribute
¶
dry_run: bool = False
--dry-run: run() prints and records the command, executes
nothing, and reports success.
env
class-attribute
instance-attribute
¶
Extra environment variables overlaid on every run() subprocess.
err_sink
class-attribute
instance-attribute
¶
err_sink: TextIO | None = None
Where this task's stderr goes. At task level it is the same buffer as
sink (so the atomic parallel flush keeps stdout/stderr in order); a
run() capturing an in-process callable temporarily points the two at
separate buffers to split the step's streams for its Result.
fetch_backend
class-attribute
instance-attribute
¶
fetch_backend: str = ''
[fetch] backend from the config ladder — which engine fetch()
downloads with. Empty means the default (stdlib urllib).
fn
class-attribute
instance-attribute
¶
fn: Any = None
The running task's own function — what inherited() reads to find
the task this one shadows. None outside a run.
force_color
class-attribute
instance-attribute
¶
force_color: bool = False
--color=always (or FORCE_COLOR): colour even when output is not a
terminal — so run() forces the tools it spawns to colour and the shown
command line paints, for a pipe into less -R. Gated off under capture
(--json), where ANSI would corrupt the envelope. Never sets the live
cursor affordances tty governs — those still need a real terminal.
in_task
class-attribute
instance-attribute
¶
in_task: bool = False
True while a task body runs (the scheduler sets it around the call),
so the interactive primitives tell a guarded mid-body call from the
framework's own up-front ask() resolution.
interactive
class-attribute
instance-attribute
¶
interactive: bool = False
@task(interactive=True): the task owns the real terminal — output is
not captured and it holds sole stdio, so its body may prompt or run a
REPL. Mid-body prompt()/confirm()/select() are allowed only here.
jobs
class-attribute
instance-attribute
¶
jobs: int = 0
The effective parallel width (-j/--jobs, config jobs, or the
cores-minus-one default) — caps parallel() pools in task bodies.
0 means unset (plain calls outside a run): no cap.
keep_going
class-attribute
instance-attribute
¶
keep_going: bool = False
This task's resolved (per-subtree) failure policy, tagged onto the subprocesses it spawns so a fail-fast failure elsewhere reaps only the fail-fast trees in a mixed run, sparing a keep-going task's.
name_width
class-attribute
instance-attribute
¶
name_width: int = 0
The widest sibling task name, so step-line columns align.
no_color
class-attribute
instance-attribute
¶
no_color: bool = False
--no-color / --color=never (or NO_COLOR): never emit ANSI styling.
no_input
class-attribute
instance-attribute
¶
no_input: bool = False
--no-input: never prompt — a required prompt errors instead of
asking, so an unattended run fails loudly rather than hanging.
passthrough
class-attribute
instance-attribute
¶
Everything after -- on the command line, verbatim.
prog
class-attribute
instance-attribute
¶
prog: str = 'fm'
The invoking CLI's command name — a branded CLI's own prog, so
tasks (the taskdocs plugin, say) can speak the brand's name.
quiet
class-attribute
instance-attribute
¶
quiet: bool = False
--quiet: suppress step lines and the per-task summary.
sequential
class-attribute
instance-attribute
¶
sequential: bool = False
The user asked for one-at-a-time (-s or config) — parallel()
honours it too. Deliberately not set by the scheduler's own
single-node routing, which is presentation, not a request to
serialise task bodies.
shell_default
class-attribute
instance-attribute
¶
shell_default: str = ''
[shell] default from the config ladder — what run(shell=True) resolves
to. Empty means posix (a POSIX shell everywhere: bash, then sh).
sink
class-attribute
instance-attribute
¶
sink: TextIO | None = None
Where this task's stdout goes: a capture buffer in buffered
(parallel) mode, None for the real stdout (live mode).
steps
class-attribute
instance-attribute
¶
Every run() this task made, in order — what recording() and
the --json envelope read.
task
class-attribute
instance-attribute
¶
task: str = ''
Who is running, for the step lines' name column: the scheduler
sets the dotted task name, parallel() its child's name. Empty
outside runs.
footman.context.RunFailed
¶
footman.context.fail
¶
Fail the current task with a reason (and exit code, default 1).
The blessed way to stop a task deliberately: fail("no open PR to act on"),
or fail("reserved branch", code=3) to pick the exit code too. A function,
not a raise, on purpose — a task lives in your repo under your linter, and
raise SomeError("a literal") trips flake8-errmsg (EM101) and tryceratops
(TRY003) at the call site, every failure. A call trips neither, the same
reason sys.exit() and pytest.fail() are functions. return N still
spells a bare code; sys.exit(...) still works — fail() is just the
footman-native one, with a reason and a code together.
footman.context.Failed
¶
Bases: Exception
A task chose to fail — the exception footman.fail() raises.
A deliberate stop with a reason (and optional exit code): the user-facing
sibling of RunFailed (which is a command's failure). Carries .reason
and .code; footman renders the reason verbatim — no type prefix — in the
failure line and the --json error field. Exported so a task can
except footman.Failed:, but fail() is the blessed way to raise it.
Composing tasks¶
footman.compose.include
¶
include(
source: str | ModuleType | Group,
/,
*,
into: Group | None = None,
only: tuple[str, ...] | list[str] = (),
exclude: tuple[str, ...] | list[str] = (),
override: bool = False,
) -> Group
Graft another module's tasks into the current tree (or into a group).
include("shared_tasks") # everything, at root
include("shared_tasks", only=["lint", "fmt"]) # cherry-pick by CLI name
include("mkdocs_helpers.tasks", into=docs) # namespace: fm docs …
include(plugin("mkdocs"), only=["build"]) # from an entry point
source is a dotted module name, an imported module, or a Group. The
provider imports under a registry capture, so its decorators can't leak
into your tree. Collisions are loud (RegistrationError) unless
override=True; unknown only=/exclude= names are errors too (typo
protection). Included tasks run from your file's directory — a shared
lint task lints this project. Returns the group it grafted into.
footman.compose.plugin
¶
The Group a package advertises under the footman.tasks entry point.
# the plugin package's pyproject.toml
[project.entry-points."footman.tasks"]
mkdocs = "footman_mkdocs:tasks"
Raises RegistrationError naming the installed entry points when name
isn't one of them — a configured-but-missing plugin should read as the
typo or missing install it is.
footman.registry.capture
¶
Redirect module-level @task/group registration into a fresh tree.
The seam include() uses to import a provider module without letting its
decorators land in the current registry: root.tasks/root.groups are
swapped for fresh dicts for the duration and the captured tree is yielded.
Reentrant — a provider may itself include() another provider.
Editing the discovered tree¶
@finalize runs a hook over the fully-merged cascade at discovery — see
Composing tasks. The hook is
handed a Tasks view; iterating or indexing it yields a TaskView that reads
and edits one task.
footman.registry.Tasks
¶
footman.registry.TaskView
¶
A finalizer's handle on one task: read its wiring, its policy flags, and
its cascade provenance (where it was defined, what it overrode), and edit it
here — never through the private _footman_* attributes.
defining_dir
property
¶
defining_dir: str | None
The folder the task was defined in, or None when the cascade did
not tag it (a plugin- or include()-composed task, not a cascade file).
Use it to act on tasks from one subtree of a monorepo.
group
instance-attribute
¶
The group this task lives in, or None for a top-level task — its
.name is the group's command-line spelling (e.g. docs). Use it to
disambiguate two tasks that share a leaf name across groups.
interactive
property
¶
interactive: bool
Whether the task owns the real terminal (@task(interactive=True)).
keep_going
property
¶
keep_going: bool | None
The task's declared failure policy (@task(keep_going=…)), or None
when it left the choice to the command line / the built-in default.
shadow_chain
property
¶
shadow_chain: tuple[Task, ...]
This task and every task it shadows, nearest (this one) first.
shadowed
property
¶
The task this one overrides — same name, one cascade level up — or
None if it shadows nothing.
source_file
property
¶
source_file: str | None
The file the task's function is defined in, or None when it can't
be located (a built-in or dynamically constructed function).
timed
property
¶
timed: bool
Whether the task records timing history / shows a determinate bar —
@task(progress=False) (and infinite/interactive) opt out.
add_post
¶
add_post(*tasks: TaskView | Task) -> None
Append post-tasks (views or functions), skipping any already set.
add_pre
¶
add_pre(*tasks: TaskView | Task) -> None
Prepend prerequisites (views or functions), skipping any already set.
disable
¶
disable(reason: str) -> None
Mark the task unavailable — listed with reason, refused if run.
set_opts
¶
set_opts(**overrides: Any) -> None
Set orchestration options on the task permanently, for every use —
the finalize-time counterpart to a per-use .opts(). Takes the same
options (keep_going, atomic, interactive, progress, confirm,
infinite) and rejects a task parameter with the same taught error; the
difference is that it edits the registered task rather than a per-use
proxy, so a finalizer can set a policy across a whole class of tasks. A
command-line -k/--fail-fast still wins over a set keep_going.
Custom CLI¶
footman.app.App
¶
A branded footman CLI — call run from your console-script entry.
footman.app.Brand
dataclass
¶
The names a CLI shows the user.
name is the long / display name (the --version banner); prog is
the short command name (the error prefix and hints); version is your
version string; tasks_file is the filename your users write tasks in
(config tasks still overrides it per project).
Typed-parameter helpers¶
footman.params.nosplit
module-attribute
¶
Opt a list/dict parameter OUT of comma-splitting, via Annotated:
By default a collection parameter splits a single token on commas
(--tag a,b,c -> ["a", "b", "c"]) in addition to the repeatable form
(--tag a --tag b). Mark it nosplit when a value may itself contain a comma:
then only the repeated flag adds items and --name "a,b" stays the literal
"a,b".
footman.params.suggest
¶
Attach a dynamic completer to a parameter, via Annotated:
list_projects() -> list[str] returns the candidate values. footman runs
it on the execution path — refreshing a cache the completion hot path serves
— and, when strict (the default), validates the supplied value against a
fresh call. A bare callable in Annotated is treated as suggest(fn).
footman.params.exists
module-attribute
¶
footman.params.isfile
module-attribute
¶
Require a Path parameter to name an existing file (see exists).
footman.params.isdir
module-attribute
¶
Require a Path parameter to name an existing directory (see exists).
footman.params.between
¶
Inclusive numeric bounds for an int/float parameter:
Validated eagerly with a taught error (--jobs must be between 1 and 32).
Either bound may be None for open-ended ranges. A bare range in
Annotated also works for ints, with Python's half-open semantics
(range(0, 8) accepts 0 through 7).
footman.params.env
¶
Fall back to an environment variable when the option isn't given:
Precedence is CLI > $DEPLOY_ENV > default. The env value flows through
the same coercion and validation as a command-line token. Only valid on a
parameter with a default — an env fallback makes it optional, so it
needs somewhere to fall.
footman.params.check
¶
A custom validator, run after coercion; raise ValueError to reject:
The callable receives the coerced value (each element, for collections).
Its ValueError message is shown to the user, so write it for them.
Declare a second parameter to also receive the siblings — the parameters to this one's left at their effective values (a provided value, else the parameter's own default), coerced and read-only (empty for the first parameter) — so a check can validate against another input:
def newer(v, params):
current = current_version(params["name"]) # the package named earlier
if Version(v) <= current:
raise ValueError(f"must be newer than {current}")
def release(name: str, version: Annotated[str, check(newer)]): ...
footman.params.doc
¶
Help text for one parameter, via Annotated:
One line, written for the person at the prompt. It shows in
fm --help <task>, as the option's description in shells that render
one (zsh, fish, nushell, PowerShell tooltips), and in the
fm --json --list catalog.
footman.params.ask
¶
Prompt for a parameter's value when it isn't supplied, via Annotated:
A required (defaultless) parameter marked ask() is prompted for when it
is not given on the command line and no env fills it — the answer runs
through the same coercion and validation as a CLI token, re-asking on a bad
value. Precedence stays CLI > env > default > prompt, so ask() only
prompts a parameter with no default (a default is the answer). Off a
terminal, under --no-input, or in --json it errors naming the flag
rather than hanging. secret=True hides the input (getpass); prompt="…"
overrides the question text.
Docstrings¶
Standalone (stdlib-only, no footman imports) — reusable outside footman.
footman.docstrings.parse
¶
Parse text (a raw or already-cleaned docstring) into its pieces.
footman.docstrings.Docstring
dataclass
¶
The parsed pieces of one docstring.
Shaped to grow additively (returns, raises, …) without breaking
reusers; absent pieces are empty, never None.
long
class-attribute
instance-attribute
¶
long: str = ''
Prose between the summary and the first section (Args: and
friends), structure preserved; empty when there is none.
params
class-attribute
instance-attribute
¶
Per-parameter help, keyed by the Python parameter name (not the
CLI spelling): {"fix": "apply safe fixes in place"}.
Markdown export¶
Pure functions over manifest tree nodes — see Your tasks, documented for the task-level surface.
footman.markdown.render_page
¶
render_page(
tree: dict[str, Any],
*,
path: tuple[str, ...] = (),
heading: int = 1,
flavor: str = "plain",
prog: str = "fm",
) -> str
One markdown document for the node at path (empty = whole tree).
footman.markdown.render_site
¶
render_site(
tree: dict[str, Any],
*,
path: tuple[str, ...] = (),
flavor: str = "plain",
prog: str = "fm",
) -> dict[str, str]
A linked set of files for the node at path: index.md per group
(name, help, a table of children with relative links), one file per task.
Keys are POSIX-relative paths.
Testing¶
footman.context.use_context
¶
Install ctx as the current run context for the duration of the block.
The public seam for calling tasks from other Python code — tests included:
run() and tools.* inside the block read this context instead of a
fresh default. footman.testing.recording builds on it.
footman.testing.Runner
¶
Drive a footman CLI in-process, capturing output and results.
Pass a branded App to test a custom CLI (Runner(App(prog="acme"))) —
error prefixes, --version, and hints then use that brand, exactly as
they would for real users.
invoke
¶
invoke(
args: str | list[str],
*,
tasks: Path | Group | None = None,
cwd: Path | None = None,
) -> InvokeResult
Run one command line and return everything it produced.
args is a string (shlex-split) or an argv list. tasks overrides
discovery: a Path routes through --tasks-file, a Group skips
discovery entirely (an in-memory tree, no files needed). Without it,
the normal tasks.py cascade from cwd applies. Never raises on
task failure — the code is in the Result; KeyboardInterrupt
passes through.
footman.testing.InvokeResult
dataclass
¶
Everything one Runner.invoke produced.
Named apart from the run-step Result (footman.Result, what run()
returns): this is the outcome of a whole CLI invocation — its exit code,
captured streams, and per-task TaskResults.
footman.testing.recording
¶
Capture the commands a block would run() — silently, not executing.
Yields the live step list; each run()/tools.* call inside the block
appends a Result instead of executing. In-process callables passed to
run() are skipped too — that is the point, but worth knowing. Keyword
overrides go to the underlying Context (e.g. env={...}).