3.3 KiB
3.3 KiB
name, description
| name | description |
|---|---|
| python-development-cli | CLI design patterns using typer. Use when building, nesting, or extending command-line interfaces. |
Rules
- Every package that exposes CLI commands contains a
cli.pymodule with aget_cli() -> typer.Typerfactory function. Do not create the Typer instance at import time. - The root package CLI (
cli.pyin the root package) is the composition point. It callscli.add_typer(get_subpackage_cli(), name="subpackage")to mount sub-package CLI trees as subcommands. - The root CLI defines a single
callbackfunction that runs before any command. It is responsible for shared bootstrap: load settings, configure logging, and store the settings object inctx.obj["settings"]so every sub-command can access it.
Example root CLI cli.py:
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.Contextas the first parameter and read settings viactx.obj["settings"].
Example sub-package cli.py:
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 withasyncio.run(). No business logic inside the command function. - The root package
__main__.pycallsget_cli()and runs it. Example:
from .cli import get_cli
if __name__ == "__main__":
cli = get_cli()
cli()