Pyrig’s CLI system uses dynamic command discovery to automatically register commands from Python functions. Simply define a function in your subcommands.py module, and it becomes a CLI command - no registration boilerplate required.
The CLI system works across your entire dependency chain. Commands from pyrig, intermediate libraries, and your project are all automatically discovered and registered.
Define commands that work across all packages in <package>/rig/cli/shared_subcommands.py:
pyrig/rig/cli/shared_subcommands.py
from importlib.metadata import version as _versionimport typerfrom pyrig.src.cli import project_name_from_argvdef version() -> None: """Display the current project's version.""" project_name = project_name_from_argv() typer.echo(f"{project_name} version {_version(project_name)}")
$ uv run pyrig versionpyrig version 3.1.5$ uv run myapp versionmyapp version 1.2.3
Shared commands adapt to each project’s context. The version command displays the version of the current project, not pyrig’s version.
def add_shared_subcommands() -> None: """Discover and register shared commands across dependency chain.""" # 1. Extract current package name package_name = package_name_from_argv() package = import_module(package_name) # 2. Find all packages in dependency chain all_shared_subcommands_modules = discover_equivalent_modules_across_dependents( shared_subcommands, pyrig, until_package=package, ) # 3. Register functions from each module for shared_subcommands_module in all_shared_subcommands_modules: sub_cmds = all_functions_from_module(shared_subcommands_module) for sub_cmd in sub_cmds: app.command()(sub_cmd)
Example: When running uv run myapp version:
Finds all packages depending on pyrig: [pyrig, mylib, myapp]
Imports shared_subcommands from each: pyrig.rig.cli.shared_subcommands, mylib.rig.cli.shared_subcommands, myapp.rig.cli.shared_subcommands
Registers all functions from all modules
Commands are registered in dependency order. If multiple packages define the same command name, the last one registered takes precedence.
Functions are extracted from modules using inspection:
def all_functions_from_module(module: ModuleType) -> list[FunctionType]: """Extract all functions defined in the module (not imported).""" functions = [] for name in dir(module): obj = getattr(module, name) if isinstance(obj, FunctionType): # Only include functions defined in this module if obj.__module__ == module.__name__: functions.append(obj) return functions
Only functions defined directly in the module are registered. Imported functions are excluded.
Typer uses function signatures and docstrings to generate help text:
import typerdef deploy( environment: str = typer.Option("production", help="Target environment"), force: bool = typer.Option(False, "--force", help="Skip confirmation"),) -> None: """Deploy the application to the specified environment. Builds the application and deploys it to the target environment. Requires proper credentials to be configured. """ if not force: typer.confirm(f"Deploy to {environment}?", abort=True) typer.echo(f"Deploying to {environment}...")
$ uv run myapp deploy --helpUsage: myapp deploy [OPTIONS] Deploy the application to the specified environment. Builds the application and deploys it to the target environment. Requires proper credentials to be configured.Options: --environment TEXT Target environment [default: production] --force Skip confirmation --help Show this message and exit.
"""Project-specific CLI commands.All public functions are automatically registered as CLI commands."""import typerdef deploy() -> None: """Deploy the application to production.""" typer.echo("Deploying...")def rollback(version: str) -> None: """Roll back to a previous version.""" typer.echo(f"Rolling back to {version}...")
$ uv run myapp deployDeploying...$ uv run myapp rollback v1.2.3Rolling back to v1.2.3...
Create <package>/rig/cli/shared_subcommands.py for commands that should be available in all dependent projects:
mylib/rig/cli/shared_subcommands.py
"""Shared CLI commands available to all dependent projects."""import typerfrom mylib import run_health_checkdef health() -> None: """Run health checks for all services.""" result = run_health_check() if result.ok: typer.echo("✓ All services healthy") else: typer.echo(f"✗ Health check failed: {result.error}") raise typer.Exit(1)
Now any project depending on mylib automatically has the health command: