Files
2026-08-08 11:24:44 +03:00

101 lines
3.3 KiB
Markdown

---
name: python-development-cli
description: CLI design patterns using typer. Use when building, nesting, or extending command-line interfaces.
---
## Rules
- Every package that exposes CLI commands contains a `cli.py` module with a `get_cli() -> typer.Typer` factory function. Do not create the Typer instance at import time.
- The root package CLI (`cli.py` in the root package) is the composition point. It calls `cli.add_typer(get_subpackage_cli(), name="subpackage")` to mount sub-package CLI trees as subcommands.
- The root CLI defines a single `callback` function that runs before any command. It is responsible for shared bootstrap: load settings, configure logging, and store the settings object in `ctx.obj["settings"]` so every sub-command can access it.
Example root CLI `cli.py`:
```python
import pathlib
import typer
from loguru import logger
from .service import ApplicationService
from .settings import RootSettings
def callback(
ctx: typer.Context,
env_path: pathlib.Path | None = typer.Option(
None, "--env", "-e",
help="Environment variables file location",
),
config_path: pathlib.Path | None = typer.Option(
None, "--config", "-c",
help="Config file location",
),
):
ctx.obj = {}
settings = RootSettings.load(
env_prefix="APP__",
env_file=env_path,
config_file=config_path,
)
ctx.obj["settings"] = settings
logger.configure(handlers=[{"sink": sys.stderr, "level": settings.logging.level}])
def run(ctx: typer.Context):
settings: RootSettings = ctx.obj["settings"]
service = ApplicationService(settings=settings)
asyncio.run(service.run())
def get_cli() -> typer.Typer:
from .agents import get_cli as get_agents_cli
from .cron import get_cli as get_cron_cli
cli = typer.Typer()
cli.callback()(callback)
cli.command(name="run")(run)
cli.add_typer(get_agents_cli(), name="agents")
cli.add_typer(get_cron_cli(), name="cron")
return cli
```
- Sub-package CLI commands receive `ctx: typer.Context` as the first parameter and read settings via `ctx.obj["settings"]`.
Example sub-package `cli.py`:
```python
import asyncio
import typer
def run(
ctx: typer.Context,
name: str | None = typer.Argument(default=None),
debug: bool = typer.Option(False, "-d", "--debug"),
):
# Deferred imports — heavy project modules are imported inside the command body
from mypackage.resolver import DependencyResolver
from mypackage.settings import RootSettings
from mypackage.channels.tui import TUIChannel, TUIChannelSettings
settings: RootSettings = ctx.obj["settings"]
resolver = DependencyResolver(settings=settings)
channel = TUIChannel(settings=TUIChannelSettings(debug=debug), ...)
asyncio.run(channel.run())
def get_cli() -> typer.Typer:
cli = typer.Typer()
cli.command(name="run")(run)
return cli
```
- Each CLI command is a plain Python function decorated with `cli.command(name="...")`. Keep command functions small: parse arguments, create a resolver or service from settings, and run async code with `asyncio.run()`. No business logic inside the command function.
- The root package `__main__.py` calls `get_cli()` and runs it. Example:
```python
from .cli import get_cli
if __name__ == "__main__":
cli = get_cli()
cli()
```