Common Arguments With Typer
I mostly use Typer to write CLIs in Python. I find it mildly easier than Click, but one thing I hadn’t worked out until today was how to implement common options with subcommands. That is, I had code like this::
import typer
cli = typer.Typer()
@cli.command()
def all(pinboard_token: str = typer.Option(None, envvar="PINBOARD_TOKEN")):
...
@cli.command()
def new(pinboard_token: str = typer.Option(None, envvar="PINBOARD_TOKEN")):
...
if __name__ == '__main___':
cli()
But I wanted --pinboard-token
to be an option on the main command, not on the subcommands. It’s essentially required, so I need to check for it, and didn’t want to do that multiple times. I also want to not repeat myself in each command.
Here’s the solution:
import typer
from types import SimpleNamespace
cli = typer.Typer()
@cli.callback()
def main(
ctx: typer.Context,
pinboard_token: str = typer.Option(None, envvar="PINBOARD_TOKEN"),
):
if not pinboard_token:
print(
"Missing pinboard token; pass --pinboard-token or set env[PINBOARD_TOKEN]"
)
raise typer.Exit(1)
ctx.obj = SimpleNamespace(pinboard_token = pinboard_token)
@cli.command()
def all(ctx: typer.Context):
# do something with ctx.obj.pinboard_token
@cli.command()
def new(ctx: typer.Context):
# do something with ctx.obj.pinboard_token
if __name__ == "__main__":
cli()
There are three “tricks” here that I’ve pieced together:
- Callbacks are what Typer uses to implement groups or common options.
@cli.callback()
lets me declare thepinboard_token
as a common option, and I can do any error checking (here, just making sure it exists) in the callback. Typer’s documentation suggests saving state (i.e.pinboard_token
here) as a global variable. If I wanted to do that I could stop here, but that would make testing a bit more diffuclt, so I want some sort of object to be passed to each subcommand. - Commands (and callbacks) can take a Context by declaring an argument of type
typer.Context
. This is actually a Click Context, since Typer uses Click under the hood. These contexts have aobj
member which can be anything you want: an arbitrary “user data” object. - I could define some special object, but since I just need want a simple namespace with a single member, I used SimpleNamespace. If this object grows and needs more members, I could use dataclasses or attrs or pydantic, but all of those are overkill here.