tyro icon indicating copy to clipboard operation
tyro copied to clipboard

Confusion around managing flags like `--version`

Open sbarrios93 opened this issue 11 months ago • 4 comments

I've been impressed with Tyro's capabilities so far. However, I've encountered some ergonomic challenges that I hope to get some help with.

Related Issue: This issue seems to be related to issue #89. Although I attempted the solutions suggested there, I was unable to resolve my problem.

The Problem: I'm in the process of creating a CLI with two subcommands: init and run. Each subcommand requires its own set of arguments. Additionally, I want the CLI to accept global flags such as --version or --status. Here's my initial setup:

Initial Setup

from typing import Annotated, Optional, Union

import tyro
from pydantic import BaseModel


class Init(BaseModel):
    env: str = ".env"
    force: bool = False


class Run(BaseModel):
    headless: bool
    days_back: int
    short_items: bool
    words_per_item: int


class Args(BaseModel):
    subcommand: Union[Init, Run]
    version: bool = False


def entrypoint() -> None:
    tyro.extras.set_accent_color("bright_yellow")
    args = tyro.cli(Annotated[Args, tyro.conf.OmitSubcommandPrefixes])
    print(args)

(.venv) python cli.py --help     
usage: cli.py [-h] [--version | --no-version] {init,run}

╭─ options ─────────────────────────────────────────╮
│ -h, --help        show this help message and exit │
│ --version, --no-version                           │
│                   (default: False)                │
╰───────────────────────────────────────────────────╯
╭─ subcommands ─────────────────────────────────────╮
│ {init,run}                                        │
│     init                                          │
│     run                                           │
╰───────────────────────────────────────────────────╯

Running cli.py --help displays the help text as expected. However, I've noticed that the --no-version flag is automatically generated and doesn't align with my requirements. I am looking for a way to disable this automatic generation.

Moreover, when I attempt to use the --version flag independently, it doesn't function as anticipated:

(.venv) python cli.py --version
╭─ Required options ───────────────────────────────╮
│ The following arguments are required: {init,run} │
│ ──────────────────────────────────────────────── │
│ For full helptext, run cli.py --help             │
╰──────────────────────────────────────────────────╯

This command incorrectly demands a subcommand (init or run), which should not be the case for a version check.

When running --version under one of the subcommands, an error is thrown out, which makes sense but the error doesn't.

(.venv) python cli.py init --version
╭─ Unrecognized options ───────────────────────╮
│ Unrecognized or misplaced options: --version │
│ ──────────────────────────────────────────── │
│ Perhaps you meant:                           │
│     --version, --no-version                  │
│         (default: False)                     │
│             in cli.py --help                 │
│ ──────────────────────────────────────────── │
│ For full helptext, run cli.py --help         │
╰──────────────────────────────────────────────╯

Optional Subcommands

To work around this, I tried making the subcommand optional:

import tyro
from pydantic import BaseModel


class Init(BaseModel):
    env: str = ".env"
    force: bool = False


class Run(BaseModel):
    headless: bool
    days_back: int
    short_items: bool
    words_per_item: int


class Args(BaseModel):
    subcommand: Optional[Union[Init, Run]] = None
    version: bool = False


def entrypoint() -> None:
    tyro.extras.set_accent_color("bright_yellow")
    args = tyro.cli(Annotated[Args, tyro.conf.OmitSubcommandPrefixes])
    print(args)


if __name__ == "__main__":
    entrypoint()

Applying an Optional type with default value = None allows --version to be run by itself but also appends a subcommand None option, which doesn't make sense in this case.

(.venv) python cli.py --version
subcommand=None version=True
(.venv) python cli.py --help
usage: cli.py [-h] [--version | --no-version] [{init,run,None}]

╭─ options ───────────────────────────────────────────────╮
│ -h, --help              show this help message and exit │
│ --version, --no-version                                 │
│                         (default: False)                │
╰─────────────────────────────────────────────────────────╯
╭─ optional subcommands ──────────────────────────────────╮
│ (default: None)                                         │
│ ─────────────────                                       │
│ [{init,run,None}]                                       │
│     init                                                │
│     run                                                 │
│     None                                                │
╰─────────────────────────────────────────────────────────╯

Regarding #89, I've tried modifying the alternatives stated here and here with no success.

1st try

import dataclasses
from typing import Tuple, Union

import tyro


@dataclasses.dataclass
class Checkout:
    """Check out a branch."""

    x: int


@dataclasses.dataclass
class Commit:
    """Commit something."""

    y: int


@dataclasses.dataclass
class Args:
    version: bool = False
    """Print version and exit."""


global_args, checkout_or_commit = tyro.cli(
    Tuple[
        Args,
        Union[Checkout, Commit],
    ]
)
print(global_args, checkout_or_commit)
(.venv) python cli.py  --0.version
╭─ Required options ──────────────────────────────────────────╮
│ The following arguments are required: {1:checkout,1:commit} │
│ ─────────────────────────────────────────────────────────── │
│ For full helptext, run cli.py --help                        │
╰─────────────────────────────────────────────────────────────╯

2nd try

import dataclasses
from typing import Annotated, Tuple, Union

import tyro


@dataclasses.dataclass
class Checkout:
    """Check out a branch."""

    x: int


@dataclasses.dataclass
class Commit:
    """Commit something."""

    y: int


@dataclasses.dataclass
class Args:
    version: bool = False
    """Print version and exit."""


global_args, checkout_or_commit = tyro.cli(
    Annotated[
        Tuple[
            Union[
                Checkout,
                Commit,
            ],
            Args,
        ],
        tyro.conf.OmitArgPrefixes,
        tyro.conf.OmitSubcommandPrefixes,
    ]
)
print(global_args, checkout_or_commit)
(.venv) python cli.py  --version
╭─ Required options ──────────────────────────────────────╮
│ The following arguments are required: {checkout,commit} │
│ ─────────────────────────────────────────────────────── │
│ For full helptext, run cli.py --help                    │
╰─────────────────────────────────────────────────────────╯

I'm confused here. Not sure if I'm misunderstanding something from the documentation or how to apply different options, but seems that applying flags that i) do not create --no-x versions and ii) can be applied alone outside any command, should be fairly straightforward on a CLI tool.

Thanks!

sbarrios93 avatar Mar 05 '24 02:03 sbarrios93