.. Comment: this file is automatically generated by `update_example_docs.py`. It should not be modified manually. .. _example-category-subcommands: Subcommands =========== In these examples, we show how :func:`tyro.cli` can be used to create CLI interfaces with subcommands. .. _example-01_subcommands: Subcommands are unions ---------------------- All of :mod:`tyro`'s subcommand features are built using unions over struct types (typically dataclasses). Subcommands are used to choose between types in the union; arguments are then populated from the chosen type. .. note:: For configuring subcommands beyond what can be expressed with type annotations, see :func:`tyro.conf.subcommand()`. .. code-block:: python :linenos: # 01_subcommands.py from __future__ import annotations import dataclasses import tyro @dataclasses.dataclass class Checkout: """Checkout a branch.""" branch: str @dataclasses.dataclass class Commit: """Commit changes.""" message: str if __name__ == "__main__": cmd = tyro.cli(Checkout | Commit) print(cmd) Print the helptext. This will show the available subcommands: .. raw:: html
$ python ./01_subcommands.py --help
usage: ./01_subcommands.py [-h] {checkout,commit}
╭─ options ─────────────────────────────────────╮
│ -h, --help show this help message and exit │
╰───────────────────────────────────────────────╯
╭─ subcommands ─────────────────────────────────╮
│ (required) │
│ • checkout Checkout a branch. │
│ • commit Commit changes. │
╰───────────────────────────────────────────────╯
The `commit` subcommand:
.. raw:: html
$ python ./01_subcommands.py commit --help
usage: ./01_subcommands.py commit [-h] --message STR
Commit changes.
╭─ options ──────────────────────────────────────╮
│ -h, --help show this help message and exit │
│ --message STR (required) │
╰────────────────────────────────────────────────╯
.. raw:: html
$ python ./01_subcommands.py commit --message hello
Commit(message='hello')
The `checkout` subcommand:
.. raw:: html
$ python ./01_subcommands.py checkout --help
usage: ./01_subcommands.py checkout [-h] --branch STR
Checkout a branch.
╭─ options ─────────────────────────────────────╮
│ -h, --help show this help message and exit │
│ --branch STR (required) │
╰───────────────────────────────────────────────╯
.. raw:: html
$ python ./01_subcommands.py checkout --branch main
Checkout(branch='main')
.. _example-02_subcommands_in_func:
Subcommands as function arguments
---------------------------------
A subcommand will be created for each input annotated with a union over
struct types.
.. note::
To prevent :func:`tyro.cli()` from converting a Union type into a subcommand,
use :class:`tyro.conf.AvoidSubcommands`.
.. note::
Argument ordering for subcommands can be tricky. In the example below,
``--shared-arg`` must always come *before* the subcommand. As an option for
alleviating this, see :class:`tyro.conf.CascadeSubcommandArgs`.
.. code-block:: python
:linenos:
# 02_subcommands_in_func.py
from __future__ import annotations
import dataclasses
import tyro
@dataclasses.dataclass
class Checkout:
"""Checkout a branch."""
branch: str
@dataclasses.dataclass
class Commit:
"""Commit changes."""
message: str
def main(
shared_arg: int,
cmd: Checkout | Commit = Checkout(branch="default"),
):
print(f"{shared_arg=}")
print(cmd)
if __name__ == "__main__":
tyro.cli(main)
Print the helptext. This will show the available subcommands:
.. raw:: html
$ python ./02_subcommands_in_func.py --help
usage: ./02_subcommands_in_func.py [-h] --shared-arg INT [{cmd:checkout,cmd:commit}]
╭─ options ─────────────────────────────────────────╮
│ -h, --help show this help message and exit │
│ --shared-arg INT (required) │
╰───────────────────────────────────────────────────╯
╭─ subcommands ─────────────────────────────────────╮
│ (default: cmd:checkout) │
│ • cmd:checkout Checkout a branch. │
│ • cmd:commit Commit changes. │
╰───────────────────────────────────────────────────╯
Using the default subcommand:
.. raw:: html
$ python ./02_subcommands_in_func.py --shared-arg 100
shared_arg=100
Checkout(branch='default')
Choosing a different subcommand:
.. raw:: html
$ python ./02_subcommands_in_func.py --shared-arg 100 cmd:commit --cmd.message 'Hello!'
shared_arg=100
Commit(message='Hello!')
.. _example-03_multiple_subcommands:
Sequenced subcommands
---------------------
Multiple unions over struct types are populated using a series of subcommands.
.. code-block:: python
:linenos:
# 03_multiple_subcommands.py
from __future__ import annotations
import dataclasses
from typing import Literal
import tyro
# Possible dataset configurations.
@dataclasses.dataclass
class Mnist:
binary: bool = False
"""Set to load binary version of MNIST dataset."""
@dataclasses.dataclass
class ImageNet:
subset: Literal[50, 100, 1000]
"""Choose between ImageNet-50, ImageNet-100, ImageNet-1000, etc."""
binaries: bool = False
# Possible optimizer configurations.
@dataclasses.dataclass
class Adam:
learning_rate: float = 1e-3
betas: tuple[float, float] = (0.9, 0.999)
@dataclasses.dataclass
class Sgd:
learning_rate: float = 3e-4
# Train script.
def train(
dataset: Mnist | ImageNet = Mnist(),
optimizer: Adam | Sgd = Adam(),
) -> None:
"""Example training script.
Args:
dataset: Dataset to train on.
optimizer: Optimizer to train with.
Returns:
None:
"""
print(dataset)
print(optimizer)
if __name__ == "__main__":
tyro.cli(train, config=(tyro.conf.CascadeSubcommandArgs,))
We apply the :class:`tyro.conf.CascadeSubcommandArgs` flag. This
allows arguments to be intermixed more flexibly:
.. raw:: html
$ python ./03_multiple_subcommands.py --help
usage: ./03_multiple_subcommands.py [-h] [OPTIONS] [DATASET] [OPTIMIZER]
Example training script.
╭─ options ────────────────────────────────────────────────────────────────────╮
│ -h, --help show this help message and exit │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ dataset options ────────────────────────────────────────────────────────────╮
│ (source subcommand: dataset:mnist) │
│ ──────────────────────────────────────────────────────────────────────────── │
│ --dataset.binary, --dataset.no-binary │
│ Set to load binary version of MNIST dataset. │
│ (default: False) │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ optimizer options ──────────────────────────────────────────────────────────╮
│ (source subcommand: optimizer:adam) │
│ ──────────────────────────────────────────────────────────────────────────── │
│ --optimizer.learning-rate FLOAT │
│ (default: 0.001) │
│ --optimizer.betas FLOAT FLOAT │
│ (default: 0.9 0.999) │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ subcommands ────────────────────────────────────────────────────────────────╮
│ Dataset to train on. (default: dataset:mnist) │
│ • dataset:mnist │
│ • dataset:image-net │
│ ──────────────────────────────────────────────────────────────────────────── │
│ Optimizer to train with. (default: optimizer:adam) │
│ • optimizer:adam │
│ • optimizer:sgd │
╰──────────────────────────────────────────────────────────────────────────────╯
.. raw:: html
$ python ./03_multiple_subcommands.py dataset:mnist --help
usage: ./03_multiple_subcommands.py dataset:mnist [-h] [DATASET:MNIST OPTIONS] [{optimizer:adam,optimizer:sgd}]
╭─ options ────────────────────────────────────────────────────────────────────╮
│ -h, --help show this help message and exit │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ dataset options ────────────────────────────────────────────────────────────╮
│ --dataset.binary, --dataset.no-binary │
│ Set to load binary version of MNIST dataset. (default: │
│ False) │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ optimizer options ──────────────────────────────────────────────────────────╮
│ (source subcommand: optimizer:adam) │
│ ──────────────────────────────────────────────────────────────────────────── │
│ --optimizer.learning-rate FLOAT │
│ (default: 0.001) │
│ --optimizer.betas FLOAT FLOAT │
│ (default: 0.9 0.999) │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ subcommands ────────────────────────────────────────────────────────────────╮
│ Optimizer to train with. (default: optimizer:adam) │
│ • optimizer:adam │
│ • optimizer:sgd │
╰──────────────────────────────────────────────────────────────────────────────╯
.. raw:: html
$ python ./03_multiple_subcommands.py dataset:mnist optimizer:adam --help
usage: ./03_multiple_subcommands.py dataset:mnist optimizer:adam [-h] [OPTIMIZER:ADAM OPTIONS]
╭─ options ─────────────────────────────────────────────────────────────────╮
│ -h, --help show this help message and exit │
╰───────────────────────────────────────────────────────────────────────────╯
╭─ dataset options ─────────────────────────────────────────────────────────╮
│ --dataset.binary, --dataset.no-binary │
│ Set to load binary version of MNIST dataset. (default: False) │
╰───────────────────────────────────────────────────────────────────────────╯
╭─ optimizer options ───────────────────────────────────────────────────────╮
│ --optimizer.learning-rate FLOAT │
│ (default: 0.001) │
│ --optimizer.betas FLOAT FLOAT │
│ (default: 0.9 0.999) │
╰───────────────────────────────────────────────────────────────────────────╯
.. raw:: html
$ python ./03_multiple_subcommands.py dataset:mnist optimizer:adam --optimizer.learning-rate 3e-4 --dataset.binary
Mnist(binary=True)
Adam(learning_rate=0.0003, betas=(0.9, 0.999))
.. _example-04_decorator_subcommands:
Decorator-based subcommands
---------------------------
:func:`tyro.extras.SubcommandApp()` provides a decorator-based API for
subcommands, inspired by `Typer
$ python 04_decorator_subcommands.py --help
usage: 04_decorator_subcommands.py [-h] {greet,addition}
╭─ options ─────────────────────────────────────╮
│ -h, --help show this help message and exit │
╰───────────────────────────────────────────────╯
╭─ subcommands ─────────────────────────────────╮
│ (required) │
│ • greet Greet someone. │
│ • addition (sum) │
│ Add two numbers. │
╰───────────────────────────────────────────────╯
.. raw:: html
$ python 04_decorator_subcommands.py greet --help
usage: 04_decorator_subcommands.py greet [-h] --name STR [--loud | --no-loud]
Greet someone.
╭─ options ──────────────────────────────────────────╮
│ -h, --help show this help message and exit │
│ --name STR (required) │
│ --loud, --no-loud (default: False) │
╰────────────────────────────────────────────────────╯
.. raw:: html
$ python 04_decorator_subcommands.py greet --name Alice
Hello, Alice!
.. raw:: html
$ python 04_decorator_subcommands.py greet --name Bob --loud
HELLO, BOB!
.. raw:: html
$ python 04_decorator_subcommands.py addition --help
usage: 04_decorator_subcommands.py addition [-h] --a INT --b INT
Add two numbers.
╭─ options ───────────────────────────────────╮
│ -h, --help show this help message and exit │
│ --a INT (required) │
│ --b INT (required) │
╰─────────────────────────────────────────────╯
.. raw:: html
$ python 04_decorator_subcommands.py addition --a 5 --b 3
5 + 3 = 8
.. raw:: html
$ python 04_decorator_subcommands.py sum --a 5 --b 3 '#' via alias
╭─ Unrecognized options ────────────────────────────────────╮
│ Unrecognized options: #, via, alias │
│ ───────────────────────────────────────────────────────── │
│ For full helptext, run 04_decorator_subcommands.py --help │
╰───────────────────────────────────────────────────────────╯
.. _example-05_subcommands_func:
Subcommands from functions
--------------------------
We provide a shorthand for generating a subcommand CLI from a dictionary. This
is a thin wrapper around :func:`tyro.cli()`'s more verbose, type-based API. If
more generality is needed, the internal working are explained in the docs for
:func:`tyro.extras.subcommand_cli_from_dict()`.
.. code-block:: python
:linenos:
# 05_subcommands_func.py
import tyro
def checkout(branch: str) -> None:
"""Check out a branch."""
print(f"{branch=}")
def commit(message: str, all: bool = False) -> None:
"""Make a commit."""
print(f"{message=} {all=}")
if __name__ == "__main__":
tyro.extras.subcommand_cli_from_dict(
{
"checkout": checkout,
"commit": commit,
}
)
.. raw:: html
$ python ./05_subcommands_func.py --help
usage: ./05_subcommands_func.py [-h] {checkout,commit}
╭─ options ─────────────────────────────────────╮
│ -h, --help show this help message and exit │
╰───────────────────────────────────────────────╯
╭─ subcommands ─────────────────────────────────╮
│ (required) │
│ • checkout Check out a branch. │
│ • commit Make a commit. │
╰───────────────────────────────────────────────╯
.. raw:: html
$ python ./05_subcommands_func.py commit --help
usage: ./05_subcommands_func.py commit [-h] --message STR [--all | --no-all]
Make a commit.
╭─ options ────────────────────────────────────────╮
│ -h, --help show this help message and exit │
│ --message STR (required) │
│ --all, --no-all (default: False) │
╰──────────────────────────────────────────────────╯
.. raw:: html
$ python ./05_subcommands_func.py commit --message hello --all
message='hello' all=True
.. raw:: html
$ python ./05_subcommands_func.py checkout --help
usage: ./05_subcommands_func.py checkout [-h] --branch STR
Check out a branch.
╭─ options ─────────────────────────────────────╮
│ -h, --help show this help message and exit │
│ --branch STR (required) │
╰───────────────────────────────────────────────╯
.. raw:: html
$ python ./05_subcommands_func.py checkout --branch main
branch='main'
.. _example-06_nested_subcommands:
Nested subcommands
------------------
Unions over struct types can be nested to create hierarchical subcommand
structures. By default, nested unions wrapped in ``Annotated`` are flattened
into the parent union. To create a named subcommand group, annotate the nested
union with :func:`tyro.conf.subcommand(name=...)`.
.. code-block:: python
:linenos:
# 06_nested_subcommands.py
from __future__ import annotations
import dataclasses
from typing_extensions import Annotated
import tyro
@dataclasses.dataclass
class Checkout:
"""Checkout a branch."""
branch: str
@dataclasses.dataclass
class Commit:
"""Commit changes."""
message: str
@dataclasses.dataclass
class Push:
"""Push commits to a remote."""
remote: str = "origin"
branch: str = "main"
@dataclasses.dataclass
class Pull:
"""Pull commits from a remote."""
remote: str = "origin"
Remote = Annotated[
Push | Pull,
tyro.conf.subcommand(name="remote", description="Remote operations."),
]
if __name__ == "__main__":
cmd = tyro.cli(Checkout | Commit | Remote)
print(cmd)
Show top-level subcommands:
.. raw:: html
$ python ./06_nested_subcommands.py --help
usage: ./06_nested_subcommands.py [-h] {checkout,commit,remote}
╭─ options ─────────────────────────────────────╮
│ -h, --help show this help message and exit │
╰───────────────────────────────────────────────╯
╭─ subcommands ─────────────────────────────────╮
│ (required) │
│ • checkout Checkout a branch. │
│ • commit Commit changes. │
│ • remote Remote operations. │
╰───────────────────────────────────────────────╯
The `checkout` subcommand:
.. raw:: html
$ python ./06_nested_subcommands.py checkout --branch main
Checkout(branch='main')
`remote` is a named group containing `push` and `pull`:
.. raw:: html
$ python ./06_nested_subcommands.py remote --help
usage: ./06_nested_subcommands.py remote [-h] {push,pull}
Remote operations.
╭─ options ───────────────────────────────────╮
│ -h, --help show this help message and exit │
╰─────────────────────────────────────────────╯
╭─ subcommands ───────────────────────────────╮
│ (required) │
│ • push Push commits to a remote. │
│ • pull Pull commits from a remote. │
╰─────────────────────────────────────────────╯
.. raw:: html
$ python ./06_nested_subcommands.py remote push --remote origin --branch main
Push(remote='origin', branch='main')
.. raw:: html
$ python ./06_nested_subcommands.py remote pull --remote origin
Pull(remote='origin')
.. _example-07_decorator_nested:
Nested decorator-based subcommands
----------------------------------
A :class:`tyro.extras.SubcommandApp` can be registered as a subcommand on
another :class:`tyro.extras.SubcommandApp`, producing a hierarchical CLI. Use
``aliases=`` for short names and ``is_default=True`` to pick which subcommand
runs when none is named.
.. code-block:: python
:linenos:
# 07_decorator_nested.py
from tyro.extras import SubcommandApp
# Nested app for database commands.
db = SubcommandApp()
@db.command(aliases=["m"])
def migrate(version: int = 1) -> None:
"""Apply schema migrations."""
print(f"migrating to version {version}")
@db.command
def seed(rows: int = 10) -> None:
"""Populate with seed data."""
print(f"seeding {rows} rows")
# Top-level app.
app = SubcommandApp()
app.command(db, name="db")
@app.command(is_default=True)
def greet(name: str = "world") -> None:
"""Default action when no subcommand is given."""
print(f"hello {name}")
if __name__ == "__main__":
app.cli()
.. raw:: html
$ python 07_decorator_nested.py --help
usage: 07_decorator_nested.py [-h] [{db,greet}]
╭─ options ───────────────────────────────────────────────╮
│ -h, --help show this help message and exit │
╰─────────────────────────────────────────────────────────╯
╭─ subcommands ───────────────────────────────────────────╮
│ (default: greet) │
│ • db │
│ • greet Default action when no subcommand is given. │
╰─────────────────────────────────────────────────────────╯
.. raw:: html
$ python 07_decorator_nested.py db --help
usage: 07_decorator_nested.py db [-h] {migrate,seed}
╭─ options ────────────────────────────────────╮
│ -h, --help show this help message and exit │
╰──────────────────────────────────────────────╯
╭─ subcommands ────────────────────────────────╮
│ (required) │
│ • migrate (m) │
│ Apply schema migrations. │
│ • seed Populate with seed data. │
╰──────────────────────────────────────────────╯
.. raw:: html
$ python 07_decorator_nested.py db migrate --version 7
migrating to version 7
.. raw:: html
$ python 07_decorator_nested.py db seed --rows 5
seeding 5 rows
.. raw:: html
$ python 07_decorator_nested.py greet --name Alice
hello Alice