Coverage for src/footman/app.py: 100%
21 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-24 13:38 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-24 13:38 +0000
1"""Public application entry: build a custom-branded CLI on top of footman.
3footman's own `fm` / `footman` commands are just the default-branded
4`App`. Point your own console script at an `App` carrying your
5project's names and version, and every message the user sees — errors,
6`--version`, the completion hint — uses them:
8```python
9# acme/cli.py
10from footman import App
12app = App(name="Acme", prog="acme", version="1.4.0")
14def main() -> None:
15 raise SystemExit(app.run())
16```
18```toml
19# your pyproject.toml
20[project.scripts]
21acme = "acme.cli:main"
22```
24Tasks are discovered exactly as they are for `fm`: the `tasks.py` cascade
25from the repo root down to the current directory.
27This module is kept import-light so the completion hot path stays fast: nothing
28here imports the registry, the manifest, or the execution layer at module load —
29those are deferred into `App.run`.
30"""
32from __future__ import annotations
34import sys
35from dataclasses import dataclass
37from footman import __version__
40@dataclass(frozen=True)
41class Brand:
42 """The names a CLI shows the user.
44 `name` is the long / display name (the `--version` banner); `prog` is
45 the short command name (the error prefix and hints); `version` is *your*
46 version string; `tasks_file` is the filename your users write tasks in
47 (config `tasks` still overrides it per project).
48 """
50 name: str = "footman"
51 prog: str = "fm"
52 version: str = __version__
53 tasks_file: str = "tasks.py"
56DEFAULT_BRAND = Brand()
59class App:
60 """A branded footman CLI — call `run` from your console-script entry."""
62 def __init__(
63 self,
64 name: str = "footman",
65 prog: str = "fm",
66 version: str | None = None,
67 tasks_file: str = "tasks.py",
68 ) -> None:
69 self.brand = Brand(
70 name=name,
71 prog=prog,
72 version=version or __version__,
73 tasks_file=tasks_file,
74 )
76 def run(self, argv: list[str] | None = None) -> int:
77 """Resolve and run the CLI, returning the process exit code.
79 Handles the stdlib-only `--complete` hot path before importing the
80 framework, so completion stays fast even through a custom entry point.
81 """
82 args = list(sys.argv[1:] if argv is None else argv)
83 if args and args[0] == "--complete":
84 from footman._complete import complete_cli
86 return complete_cli(args[1:])
87 from footman import _app
89 return _app.run(args, brand=self.brand)