Subcommands¶
In these examples, we show how tyro.cli()
can be used to create CLI
interfaces with subcommands.
Subcommands are Unions¶
All of 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
tyro.conf.subcommand()
.
1# 01_subcommands.py
2from __future__ import annotations
3
4import dataclasses
5
6import tyro
7
8@dataclasses.dataclass(frozen=True)
9class Checkout:
10 """Checkout a branch."""
11
12 branch: str
13
14@dataclasses.dataclass(frozen=True)
15class Commit:
16 """Commit changes."""
17
18 message: str
19
20if __name__ == "__main__":
21 cmd = tyro.cli(Checkout | Commit)
22 print(cmd)
Print the helptext. This will show the available subcommands:
$ python ./01_subcommands.py --help usage: 01_subcommands.py [-h] {checkout,commit} ╭─ options ───────────────────────────────────────────────╮ │ -h, --help show this help message and exit │ ╰─────────────────────────────────────────────────────────╯ ╭─ subcommands ───────────────────────────────────────────╮ │ {checkout,commit} │ │ checkout Checkout a branch. │ │ commit Commit changes. │ ╰─────────────────────────────────────────────────────────╯
The commit subcommand:
$ 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) │ ╰──────────────────────────────────────────────────────╯
$ python ./01_subcommands.py commit --message hello
Commit(message='hello')
The checkout subcommand:
$ 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) │ ╰─────────────────────────────────────────────────────╯
$ python ./01_subcommands.py checkout --branch main
Checkout(branch='main')
Subcommands as Function Arguments¶
A subcommand will be created for each input annotated with a union over struct types.
Note
To prevent tyro.cli()
from converting a Union type into a subcommand,
use 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 tyro.conf.ConsolidateSubcommandArgs
.
1# 02_subcommands_in_func.py
2from __future__ import annotations
3
4import dataclasses
5
6import tyro
7
8@dataclasses.dataclass(frozen=True)
9class Checkout:
10 """Checkout a branch."""
11
12 branch: str
13
14@dataclasses.dataclass(frozen=True)
15class Commit:
16 """Commit changes."""
17
18 message: str
19
20def main(
21 shared_arg: int,
22 cmd: Checkout | Commit = Checkout(branch="default"),
23):
24 print(f"{shared_arg=}")
25 print(cmd)
26
27if __name__ == "__main__":
28 tyro.cli(main)
Print the helptext. This will show the available subcommands:
$ 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) │ ╰─────────────────────────────────────────────────────────╯ ╭─ optional subcommands ──────────────────────────────────╮ │ (default: cmd:checkout) │ │ ────────────────────────────────────────── │ │ [{cmd:checkout,cmd:commit}] │ │ cmd:checkout Checkout a branch. │ │ cmd:commit Commit changes. │ ╰─────────────────────────────────────────────────────────╯
Using the default subcommand:
$ python ./02_subcommands_in_func.py --shared-arg 100
shared_arg=100
Checkout(branch='default')
Choosing a different subcommand:
$ python ./02_subcommands_in_func.py --shared-arg 100 cmd:commit --cmd.message 'Hello!'
shared_arg=100
Commit(message='Hello!')
Sequenced Subcommands¶
Multiple unions over struct types are populated using a series of subcommands.
1# 03_multiple_subcommands.py
2from __future__ import annotations
3
4import dataclasses
5from typing import Literal
6
7import tyro
8
9# Possible dataset configurations.
10
11@dataclasses.dataclass
12class Mnist:
13 binary: bool = False
14 """Set to load binary version of MNIST dataset."""
15
16@dataclasses.dataclass
17class ImageNet:
18 subset: Literal[50, 100, 1000]
19 """Choose between ImageNet-50, ImageNet-100, ImageNet-1000, etc."""
20
21# Possible optimizer configurations.
22
23@dataclasses.dataclass
24class Adam:
25 learning_rate: float = 1e-3
26 betas: tuple[float, float] = (0.9, 0.999)
27
28@dataclasses.dataclass
29class Sgd:
30 learning_rate: float = 3e-4
31
32# Train script.
33
34def train(
35 dataset: Mnist | ImageNet = Mnist(),
36 optimizer: Adam | Sgd = Adam(),
37) -> None:
38 """Example training script.
39
40 Args:
41 dataset: Dataset to train on.
42 optimizer: Optimizer to train with.
43
44 Returns:
45 None:
46 """
47 print(dataset)
48 print(optimizer)
49
50if __name__ == "__main__":
51 tyro.cli(train, config=(tyro.conf.ConsolidateSubcommandArgs,))
We apply the tyro.conf.ConsolidateSubcommandArgs
flag. This
pushes all arguments to the end of the command:
$ python ./03_multiple_subcommands.py --help usage: 03_multiple_subcommands.py [-h] {dataset:mnist,dataset:image-net} Example training script. ╭─ options ─────────────────────────────────────────╮ │ -h, --help show this help message and exit │ ╰───────────────────────────────────────────────────╯ ╭─ subcommands ─────────────────────────────────────╮ │ Dataset to train on. │ │ ───────────────────────────────── │ │ {dataset:mnist,dataset:image-net} │ │ dataset:mnist │ │ dataset:image-net │ ╰───────────────────────────────────────────────────╯
$ python ./03_multiple_subcommands.py dataset:mnist --help usage: 03_multiple_subcommands.py dataset:mnist [-h] {optimizer:adam,optimizer:sgd} ╭─ options ─────────────────────────────────────────╮ │ -h, --help show this help message and exit │ ╰───────────────────────────────────────────────────╯ ╭─ subcommands ─────────────────────────────────────╮ │ Optimizer to train with. │ │ ────────────────────────────── │ │ {optimizer:adam,optimizer:sgd} │ │ optimizer:adam │ │ optimizer:sgd │ ╰───────────────────────────────────────────────────╯
$ python ./03_multiple_subcommands.py dataset:mnist optimizer:adam --help usage: 03_multiple_subcommands.py dataset:mnist optimizer:adam [-h] [--optimizer.learning-rate FLOAT] [--optimizer.betas FLOAT FLOAT] [--dataset.binary | --dataset.no-binary] ╭─ options ─────────────────────────────────────────────────────────╮ │ -h, --help │ │ show this help message and exit │ ╰───────────────────────────────────────────────────────────────────╯ ╭─ optimizer options ───────────────────────────────────────────────╮ │ --optimizer.learning-rate FLOAT │ │ (default: 0.001) │ │ --optimizer.betas FLOAT FLOAT │ │ (default: 0.9 0.999) │ ╰───────────────────────────────────────────────────────────────────╯ ╭─ dataset options ─────────────────────────────────────────────────╮ │ --dataset.binary, --dataset.no-binary │ │ Set to load binary version of MNIST dataset. (default: False) │ ╰───────────────────────────────────────────────────────────────────╯
$ 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))
Decorator-based Subcommands¶
tyro.extras.SubcommandApp()
provides a decorator-based API for
subcommands, which is inspired by click.
1# 04_decorator_subcommands.py
2from tyro.extras import SubcommandApp
3
4app = SubcommandApp()
5
6@app.command
7def greet(name: str, loud: bool = False) -> None:
8 """Greet someone."""
9 greeting = f"Hello, {name}!"
10 if loud:
11 greeting = greeting.upper()
12 print(greeting)
13
14@app.command(name="addition")
15def add(a: int, b: int) -> None:
16 """Add two numbers."""
17 print(f"{a} + {b} = {a + b}")
18
19if __name__ == "__main__":
20 app.cli()
$ python 04_decorator_subcommands.py --help usage: 04_decorator_subcommands.py [-h] {greet,addition} ╭─ options ───────────────────────────────────────────────╮ │ -h, --help show this help message and exit │ ╰─────────────────────────────────────────────────────────╯ ╭─ subcommands ───────────────────────────────────────────╮ │ {greet,addition} │ │ greet Greet someone. │ │ addition Add two numbers. │ ╰─────────────────────────────────────────────────────────╯
$ 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) │ ╰─────────────────────────────────────────────────────────╯
$ python 04_decorator_subcommands.py greet --name Alice
Hello, Alice!
$ python 04_decorator_subcommands.py greet --name Bob --loud
HELLO, BOB!
$ 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) │ ╰───────────────────────────────────────────────────╯
$ python 04_decorator_subcommands.py addition --a 5 --b 3
5 + 3 = 8
Subcommands from Functions¶
We provide a shorthand for generating a subcommand CLI from a dictionary. This
is a thin wrapper around tyro.cli()
’s more verbose, type-based API. If
more generality is needed, the internal working are explained in the docs for
tyro.extras.subcommand_cli_from_dict()
.
1# 05_subcommands_func.py
2import tyro
3
4def checkout(branch: str) -> None:
5 """Check out a branch."""
6 print(f"{branch=}")
7
8def commit(message: str, all: bool = False) -> None:
9 """Make a commit."""
10 print(f"{message=} {all=}")
11
12if __name__ == "__main__":
13 tyro.extras.subcommand_cli_from_dict(
14 {
15 "checkout": checkout,
16 "commit": commit,
17 }
18 )
$ python ./05_subcommands_func.py --help usage: 05_subcommands_func.py [-h] {checkout,commit} ╭─ options ───────────────────────────────────────────────╮ │ -h, --help show this help message and exit │ ╰─────────────────────────────────────────────────────────╯ ╭─ subcommands ───────────────────────────────────────────╮ │ {checkout,commit} │ │ checkout Check out a branch. │ │ commit Make a commit. │ ╰─────────────────────────────────────────────────────────╯
$ 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) │ ╰────────────────────────────────────────────────────────╯
$ python ./05_subcommands_func.py commit --message hello --all
message='hello' all=True
$ 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) │ ╰─────────────────────────────────────────────────────╯
$ python ./05_subcommands_func.py checkout --branch main
branch='main'