brentyi / tyro

Zero-effort CLI interfaces & config objects, from types
https://brentyi.github.io/tyro
MIT License
457 stars 23 forks source link

multiple independent subcommands #61

Open Linusnie opened 1 year ago

Linusnie commented 1 year ago

hi, first off thanks for making this great library! I've run into the following issue: I'd like to have a setting structure with two subcommands, each with separate settings and defaults:

from dataclasses import dataclass
from typing import Union
import tyro

@dataclass
class A1:
    a1: int = 1

@dataclass
class A2:
    a2: int = 1

@dataclass
class B1:
    b1: int = 1

@dataclass
class B2:
    b2: int = 1

@dataclass
class Config:
    a: Union[A1, A2] = A1()
    b: Union[B1, B2] = B1()

if __name__ == '__main__':
    print(tyro.cli(Config))

I'd like to be able to set the value for config.b while leaving config.a as its default. However if I run python tyro_test.py b:b2 I get the following error:

tyro_test.py: error: argument [{a:a1,a:a2}]: invalid choice: 'b:b2' (choose from 'a:a1', 'a:a2')

and the only way(?) to set config.b is to set config.a first with python tyro_test.py a:a1 b:b2. Is there a way to set config.b via the command line without having to set config.a?

brentyi commented 1 year ago

Hi @Linusnie!

I appreciate the detailed issue.

Unfortunately, what you described is the best that we can do right now. I think https://github.com/brentyi/tyro/issues/60#issuecomment-1656369002 addresses the same problem:

This is one of a handful of remaining things in tyro that I'm not super happy with; it mostly reduces to the limited choices we have when mapping types to argparse functionality, which we use for all of the low-level CLI stuff.

For union types over structures like dataclasses, subparsers make sense because they let us only accept arguments from the chosen types.

The downside is that each parser or subparser can only contain a single set of "child" subparsers. This results in a tree structure that doesn't map exactly to the case where we have multiple Union types. In your case, the tree structure we generate looks like:

# (subcommands have been shortened a bit for brevity)

[root parser]
├─ output-head:None
│  ├─ optimizer:None
│  ├─ optimizer:optimizer
├─ output-head:output-head
│  ├─ optimizer:None
│  ├─ optimizer:optimizer

...where optimizer:* is always actually a subcommand of the output-head:*, and not the root parser. And so we need to pass in output-head:* in order to traverse a step down the tree, where we can then pass in the optimizer:* subcommand.

Of course open to suggestions here. :)

The best I've been able to come up with is to tear out argparse and replace it with an in-house solution — or try to fork argparse to somehow add this functionality — but of course either would take a lot of development energy, while breaking compatibility with argparse-reliant tools (for example, we use shtab for tab completion in the shell).

If you really want this behavior, I guess you could annotate the first field with Union[A1, A2, B1, B2], and add some post-processing logic, but this would also add a lot of complexity + downsides.

Linusnie commented 1 year ago

@brentyi I see, thank you for the explanation! That's unfortunate but I'll try to reduce it to just one subcommand somehow for now then