multiple independent subcommands
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?
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
tyrothat I'm not super happy with; it mostly reduces to the limited choices we have when mapping types toargparsefunctionality, 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 theoutput-head:*, and not the root parser. And so we need to pass inoutput-head:*in order to traverse a step down the tree, where we can then pass in theoptimizer:*subcommand.Of course open to suggestions here. :)
The best I've been able to come up with is to tear out
argparseand replace it with an in-house solution — or try to forkargparseto somehow add this functionality — but of course either would take a lot of development energy, while breaking compatibility withargparse-reliant tools (for example, we useshtabfor 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.
@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