-
Notifications
You must be signed in to change notification settings - Fork 24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add arguments when subcommands are used #89
Comments
I saw #90, thanks for the fix, but it recognizes
|
Hi! I've been meaning to reply but am still ironing out a few more edge cases that your example (and the recent introduction of I appreciate you filing this issue; to clarify, For the Union semantics: I can see why it's confusing, but what So when we write: @dataclasses.dataclass
class Args1:
x: int
@dataclasses.dataclass
class Args2:
y: int what To clarify your example, when we write the union what we mean is: # Choose and instantiate one of...
Union[
# (1) An `Any` type, using `checkout` as a constructor.
Annotated[
Any,
tyro.conf.subcommand(name="checkout", constructor=checkout),
],
# (2) An `Any` type, using `commit` as a constructor.
Annotated[
Any,
tyro.conf.subcommand(name="commit", constructor=commit),
],
# (3) An `Arg` type, using its default constructor.
Arg,
] ...which results in 3 subcommands. Does that make sense to you? |
Thanks for your reply. But I am not sure if this has the correct semantics. Simply doing the following without Unions
will yield
It sounds like the semantics of If All that said, then how should I do in order to achieve the behavior I would want? ( |
Sorry in advance for the length here; if anything's unclear please do let me know!
Yes, this is basically true and (to me) follows naturally given the meaning of a Union type. In the case of def main(x: Union[int, str]) -> None:
...
tyro.cli(main) where In the case of
I'm not really following this, could you elaborate? In the current setup if
I think it's not 100% clear to me how exactly you want to use your global arguments, but here are some patterns that might be helpful: import tyro
from typing import Union
import dataclasses
@dataclasses.dataclass
class Subcommand1:
a: int
@dataclasses.dataclass
class Subcommand2:
b: int
@dataclasses.dataclass
class Args:
subcommand: Union[Subcommand1, Subcommand2]
foo: int
tyro.cli(Args) import dataclasses
from typing import Union
import tyro
@dataclasses.dataclass
class Subcommand1:
a: int
@dataclasses.dataclass
class Subcommand2:
b: int
@dataclasses.dataclass
class Args:
foo: int
def main(args: Args, subcommand: Union[Subcommand1, Subcommand2]) -> None:
...
tyro.cli(main) import dataclasses
from typing import Union
import tyro
@dataclasses.dataclass
class SharedArgs:
foo: int
@dataclasses.dataclass
class Subcommand1(SharedArgs):
a: int
@dataclasses.dataclass
class Subcommand2(SharedArgs):
b: int
subcommand = tyro.cli(Union[Subcommand1, Subcommand2]) |
Thank you very, very much for your explanation and a very comprehensive, thoughtful answer! I really appreciate your time and efforts on my issue and the great project as well :)
Just to clarify what I wanted to say here:
Yes, I can see why we had such a design and semantics on However, in order to implement what is requested in this issue (global arguments) we'll need a bit more sophisticated mechanism. Let me first elaborate on a use case that I'd like to achieve here. Basically, I'd like to add some "global arguments" that is not tied with any subparsers: usage: 89.py [-h] {checkout,commit}
╭─ arguments ─────────────────────────────────────────────╮
│ -h, --help show this help message and exit │
+| --version print version and exit │ <--- add here, something like this
╰─────────────────────────────────────────────────────────╯
╭─ subcommands ───────────────────────────────────────────╮
│ {checkout,commit,arg} │
│ checkout Check out a branch. │
│ commit Make a commit. │
╰─────────────────────────────────────────────────────────╯ Let's say we are building a git-like program with some subcommands and global options without subcommand:
Some global options like (1) One idea is to have a special typing object: tyro.cli(tyro.Subcommands(
Arg1, Arg2,
arguments=GlobalArgs,
])
# .. or
tyro.cli(tyro.Subcommands(
Union[
Annotated[Any, tyro.conf.subcommand(name="checkout", constructor=checkout),
Annotated[Any, tyro.conf.subcommand(name="commit", constructor=commit),
],
arguments=GlobalArgs,
)) which would be more explicit than tyro.cli(Annotated[
Union[...],
tyro.conf.????(arguments=GlobalArgs)
]) although I don't like the use of (2) Or use something like unboxing or unwrapping (out of subcommand), i.e., add a metadata like tyro.cli(Union[
Annotated[Any, tyro.conf.subcommand(name="checkout", constructor=checkout),
Annotated[Any, tyro.conf.subcommand(name="commit", constructor=commit),
# Note the new field unwrap=True (or this can be implicitly assumed by having an "empty" name without the new field)
Annotated[Any, tyro.conf.subcommand(name="", constructor=global_args, unwrap=True)
]) (though we need a better name that is much clearer than this) which will not create a subparser but add arguments to the main parser. NOTE:
What do you think? Thanks a lot again! |
I'm not sure I agree with this. Assuming no inheritance or structural subtyping ( x = tyro.cli(Union[Args1, Args2]) it's not possible for both I also appreciate you taking the time to write out some suggestions, it was helpful! I think a core question for me is: how would you want to access the global arguments? If you wrote: x = tyro.cli(tyro.Subcommands(
Arg1, Arg2,
arguments=GlobalArgs,
]) it's not clear to me what I do think the core idea of wanting to instantiate both (1) For example you can use tyro to instantiate a tuple that contains exactly that: 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(
Tuple[
Args,
Union[Checkout, Commit],
]
)
print(global_args, checkout_or_commit) This should also type check correctly in A downside is that nesting things in global_args, checkout_or_commit = tyro.cli(
Tuple[
Annotated[Args, tyro.conf.arg(name="")],
Annotated[Union[Checkout, Commit], tyro.conf.arg(name="")],
]
) Where the end result is:
Does this seem consistent with your desired behavior? |
Thanks for the response.
But yes, only under some assumptions --- like no inheritance or structural subtyping. In general Even more simpler example: The idea of using Tuple[] (or even nested dataclass or namedtuple) is brilliant! Here, the semantic is "conjunction" rather than "disjunction" (Union), so this is exactly what I want. The trick is to use A fully reproducible codeimport dataclasses
from pathlib import Path
from typing import Annotated, Tuple, Union
import tyro.conf
@dataclasses.dataclass
class Checkout:
"""Check out a branch."""
branch: str
@dataclasses.dataclass
class Commit:
"""Commit something."""
input: tyro.conf.Positional[Path]
@dataclasses.dataclass
class Arg:
verbose: bool = True
def case1():
# 1. OK
o = tyro.cli(
Union[
Checkout,
Commit,
],
args=["commit", "./path.txt"]
)
# should be Commit(input=PosixPath('path.txt'))
print(o)
def case2():
arg, action = tyro.cli(Tuple[
Arg,
Annotated[
Union[
Checkout,
Commit,
],
tyro.conf.arg(name=""),
]
], args=["commit", "./path.txt"])
# should be Commit(input=PosixPath('path.txt'))
assert isinstance(arg, Arg)
assert isinstance(action, Commit)
print(action)
def case3():
# 3. Error
# tyro._instantiators.UnsupportedTypeAnnotationError:
# Expected fully type-annotated callable, but <function Singleton.__new__ at 0x103058720>
# with arguments ('args', 'kwds') has no annotation for 'args'.
o = tyro.cli(Tuple[
Annotated[
Arg,
tyro.conf.arg(name=""),
],
Annotated[
Union[
Annotated[Checkout, tyro.conf.subcommand(name="checkout")],
Annotated[Commit, tyro.conf.subcommand(name="commit")],
],
tyro.conf.arg(name=""),
]
# ], args=["--help"])
], args=["commit", "./path.txt"])
print(o)
def case4():
# Weird type eerror
# tyro._instantiators.UnsupportedTypeAnnotationError:
# Unsupported type annotation for the field .
# To suppress this error, assign the field a default value.
#
# Tuple[T1] -> maybe this is an invalid type definition?
o = tyro.cli(Tuple[
Annotated[
Union[
Annotated[Checkout, tyro.conf.subcommand(name="checkout")],
Annotated[Commit, tyro.conf.subcommand(name="commit")],
],
tyro.conf.arg(name=""),
]
], args=["commit", "./path.txt"])
print(o)
if __name__ == '__main__':
# case1()
# case2()
case3()
# case4() The error is:
|
Got it, agree with what you wrote about union semantics! I also agree that the For avoiding the cryptic global_args, checkout_or_commit = tyro.cli(
Annotated[
Tuple[
Union[
Checkout,
Commit,
],
Args,
],
tyro.conf.OmitArgPrefixes,
tyro.conf.OmitSubcommandPrefixes,
]
) |
Thanks for the tip |
Yep, all of the |
Example: https://brentyi.github.io/tyro/examples/02_nesting/05_subcommands_func/
I would like to have a global option to the program, not options to specific subcommands, say
app.py --verbose
,app.py --version
, orapp.py --config foo.bar
whilest subcommandsapp.py checkout --branch=...
app.py commit --message ...
are still working.What I tried:
But the arguments defined in the dataclass
Arg
is always unrecognizedUnrecognized arguments: --foo
when subcommands are used.The text was updated successfully, but these errors were encountered: