Closed max-hcompany closed 4 weeks ago
Hi @max-hcompany,
Thanks for the detailed issue + code samples!
To reiterate, your three questions are: (Q1) how to make the CLI more robust to argument ordering, (Q2) whether we can make argument placement error messages more descriptive, (Q3) whether we can show subcommand errors before unrecognized argument errors.
I can start with recommendations for making life easier in general when working with subcommands for configuring research code. I think our default behaviors are generally correct given practical limitations (inherited from building on argparse
) and subcommand conventions (eg, git commit -a
is different from git -a commit
) but I do recognize the UX difficulties and enormous room for improvement...
For addressing Q1, one option is tyro.conf.ConsolidateSubcommandArgs
. This will push your arguments all to the end of the subcommand list, which is generally less brittle:
config = tyro.cli(
Config,
config=(tyro.conf.ConsolidateSubcommandArgs,),
args=[
"opt:opt-b",
"opt2:opt-a",
# After here, order no longer matters:
"--opt2.lr", "0.3",
"--opt.lr", "3e-6",
"--opt.beta", "0.5",
"--name", "test",
],
)
The config=
syntax requires version 0.8.7
, which I only just released[^1].
tyro 0.8.8
contains an additional helper under tyro.extras
. This is the pattern I use for most of my own projects, particularly when collaborating with other people who I don't want to put through the trouble of understanding layers of subcommands:
default_configs = {
"ab": (
"Set opt to OptA and opt2 to OptB.",
Config(name=tyro.MISSING, opt=OptA(), opt2=OptB()),
),
"ba": (
"Set opt to OptB and opt2 to OptA.",
Config(name=tyro.MISSING, opt=OptB(), opt2=OptA()),
),
}
config = tyro.extras.overridable_config_cli(default_configs)
This will flatten everything into only a single layer of subcommands: all usage will look like python script.py {ab,ba} [--options]
. We lose some expressivity, but the simplicity tradeoffs related to your Q1 concern are usually worth it to me. There's a live example in our gsplat repo here; if you want more customizability the helper implementation is only a few lines of code (source).
[^1]: For older versions of tyro
you can do either tyro.cli(tyro.conf.configure(ConsolidateSubcommandArgs)(Config))
or tyro.cli(tyro.conf.ConsolidateSubcommandArgs[Config])
.
If you go with either of the options above hopefully the error message improvements become less critical. But I just made a commit that improves this situation based on your Q2 suggestion, that I hope to include in a release once I've tested more:
Q3 is inherited from argparse
and harder to address. The longer term fix here that I'd like to make is to move away from building on top of argparse
entirely (similar to Cappa which I think is great), but this is fairly involved and the benefits are limited.
Please let me know if I missed anything / anything's unclear!
This is incredibly helpful. Thank you so much for taking the time to reply!
I think the newer solutions you've suggested will help a ton with immediate usability, and the error messages you've provided definitely look at the step in the right direction.
Hi, thank you for your library it has been very pleasant to work with (
tyro==0.8.4
, Py3.10).I have been struggling with getting union types to feel more natural from the command line. I was wondering if there was anything I could be doing better or how we might be able to improve the error messages to help users fix their flags. Consider the following example:
This will work correctly and produce the following config
Config(name='test', opt=OptB(lr=3e-06, beta=0.5), opt2=OptA(lr=0.3))
.Apologies if I miss name or poorly describe the issue from here, but I will try my best. The pain point comes in with the special flags used to infer type
opt:opt-b
, andopt2:opt-a
. They seem to force the scope of the remaining list of flags until another special flag is used to change the scope again. This means that once one of these special flags are invoked the ordering and placement of all subsequent flags is rigid (whereas, it was previously flexible).Q1: Is it possible to decouple the type inference from the command line ordering? For example,
This would make the UX less brittle due to needing to follow strong conventions.
Q2: How can we get errors that more readily describe that flag-ordering and these typed scopes are the source of issue? For example, if you run the snippet from Q1 you will receive the error message:
First of all, these boxed help windows are beautiful. Huge fan.
Zooming on on the errors specifically we have
'tuple' object has no attribute 'tb_frame'"
andUnrecognized or misplaced options: --opt.lr --opt.beta
. Here the worldmisplaced
is doing a lot of heavy lifting in terms of abstracting away the problem and the solution. To the user, all of the flags are valid, so what does misplaced mean? Is it possible to provide the user feedback that they're scoped in toopt2:opt-a
at this time and that only those flags are valid at this time and to shift scope toopt
to specify those flags?In the case that Q1 can be implemented then this would also be not an issue. :)
Q3: Is it possible to warn the user that they've not specified the type being used from the union? For example,
Will raise the following error:
I would prefer it warn the user that
opt
has not had its type resolved betweenopt-a
andopt-b
. I think it may be frustrating to the user to see that the suggested flag that is defined is exactly the flag upon which an error is being raised. It might be useful to include in the suggestion that these flags must be scoped? (Or in the case of Q1 again, non-issue).I think this is lightly related to #60 and #61, but is a unique issue. Let me know if I misunderstood those issues.
Thanks again for this wonderful library. I hope these questions can further the UX so that I can convince more and more people to adopt tyro. :)