Commands¶
This document provides an extensive, Django-style guide to Sayer's command system. It offers detailed explanations, numerous examples, and actionable how-tos to help you fully leverage Sayer's capabilities.
What is a Command?¶
A command is a Python function decorated with @command
. Sayer automatically parses parameters using Annotated
types and manages help, execution flow, and middleware.
Key Features of Sayer Commands¶
- Decorator-based registration (
@command
). - Parameter parsing with
Annotated
types (Option, Argument, Env, JsonParam). - Sync and async support.
- Middleware integration (before/after hooks).
- Rich help output with auto-generated parameter documentation.
Step-by-Step Guide¶
Defining Basic Commands¶
from sayer import Sayer, command, Option
from typing import Annotated
app = Sayer(help="My CLI App")
@app.command()
def greet(name: Annotated[str, Option()], shout: Annotated[bool, Option()] = False):
"""Greet a user by name."""
message = f"Hello, {name}!"
if shout:
message = message.upper()
print(message)
Run:
python main.py greet --name Alice --shout
Output:
HELLO, ALICE!
Complex Parameters with Annotations¶
from sayer import Argument, Env, JsonParam
from typing import Annotated
@app.command()
def process(
input_file: Annotated[str, Argument()],
config: Annotated[dict, JsonParam()],
token: Annotated[str, Env("API_TOKEN")],
count: Annotated[int, Option()] = 1
):
print(f"File: {input_file}, Config: {config}, Token: {token}, Count: {count}")
Run:
python main.py process input.txt --config '{"key": "value"}' --token secret --count 5
Middleware Integration¶
from sayer.middleware import add_before_global
async def before(ctx):
print(f"Preparing to run {ctx.command.name}")
add_before_global(before)
Subcommands and Groups¶
from sayer import group, command
cli = group(help="Main CLI")
@cli.command()
def subcmd():
print("Subcommand executed.")
cli()
Async Commands¶
from sayer import Sayer, command
import anyio
app = Sayer()
@app.command()
async def async_greet():
await anyio.sleep(1)
print("Async Hello!")
app()
Dynamic Registration¶
from sayer.utils.loader import load_commands_from
load_commands_from("myapp.commands")
The decorator @command
¶
sayer @command
decorator enhancements¶
Recent updates allow you to specify the command name both as a positional string argument or via the name=
keyword, without interfering with signature
introspection and type inference.
Usage Patterns¶
Style | Resulting CLI name | Behavior |
---|---|---|
@command |
func-name → func-name |
Defaults to function name, snake→kebab |
@command("custom-name") |
custom-name |
Positional override |
@command(name="custom-name") |
custom-name |
Keyword override |
@command(hidden=True, short_help="…") |
func-name , hidden |
Passes any Click attrs through |
from sayer.core.engine import command
# default: snake_case name → kebab-case
@command
def show_ctx_simple(ctx: click.Context):
"""Show context"""
click.echo(ctx)
# positional name override
@command("list-things", help="List all things")
def list_things():
...
# keyword name + extra Click attrs
@command(name="push", hidden=True)
def do_push():
"""Push changes"""
...
Behind the scenes, command()
now:
- Peels off a first-positional
str
as thename
override. - Falls back to
name=func.__name__.replace('_', '-')
if no override. - Pops
help=
from attrs or usesfunc.__doc__
. - Forwards all other
**attrs
(e.g.hidden
,short_help
) into@click.command(...)
. - Still inspects the real function to build parameters via
inspect.signature
.
Sayer.add_command
improvements¶
When mounting sub-apps or groups, the behavior now distinguishes three cases:
Sayer
instance: Automatically unwraps and mounts its internalSayerGroup
.click.Group
(orSayerGroup
): Mounted as-is, preserving nested subcommands and rich help.- Leaf commands: Any other
click.Command
is wrapped inSayerCommand
to render rich help.
from sayer import Sayer
import click
root = Sayer(name="root")
sub = Sayer(name="sub")
@sub.command("run")
def run():
"""Run something"""
pass
# mounts the sub-app under 'sub'
root.add_command(sub)
root sub --help
now showsrun
undersub
.root run
remains a richSayerCommand
.
This ensures that nested apps retain their full group behavior and rich help formatting.
Custom Commands¶
Sayer now supports Custom Commands, allowing you to register extra commands outside the normal @command
decorator flow. These are useful when you want to extend the CLI with application-specific actions while keeping them visually separated in the help output.
When you run --help
, custom commands appear under a dedicated Custom section, making it clear what has been added by you versus what ships with the app.
Registering Custom Commands¶
Direct Registration¶
import click
from sayer import Sayer, command
app = Sayer(name="myapp")
@command
def shout():
"""Shout in uppercase."""
click.echo("HELLO WORLD!")
app.add_custom_command(shout, "shout")
Run:
python main.py shout
Output:
HELLO WORLD!
Help output (python main.py --help
):
Usage: myapp [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
greet Greet a user by name.
process Process a file and config.
Custom:
shout Shout in uppercase.
Why Use Custom Commands?¶
- Keeps a clear separation between framework commands and your own project-specific commands.
- Ideal for project tooling (e.g.,
init-db
,lint
,deploy
) that you don't want mixed into core Sayer groups. - Ensures consistent help formatting, even across nested apps.
Comprehensive Best Practices¶
- ✅ Use clear docstrings and parameter annotations for help output.
- ✅ Test commands with different parameters and edge cases.
- ✅ Keep commands focused; delegate complex logic to functions.
- ✅ Use groups for modular CLI structures.
- ✅ Use
JsonParam
for structured inputs. - ❌ Don't mix
Option
andArgument
on the same parameter. - ❌ Avoid hardcoding sensitive data; use
Env
for secrets.
Advanced Techniques¶
- Leverage dynamic module loading for large apps.
- Use async commands for network or I/O tasks.
- Customize middleware for logging, validation, or context prep.
- Implement complex parsers with
JsonParam
.
Visual Diagram¶
graph TD
Command[Command]
Param[Parameter Parsing]
Middleware[Middleware Hooks]
Help[Help Generation]
Command --> Param
Command --> Middleware
Command --> Help