Skip to content
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

Mutually exclusive arguments? #129

Open
dsedivec opened this issue Feb 25, 2024 · 3 comments
Open

Mutually exclusive arguments? #129

dsedivec opened this issue Feb 25, 2024 · 3 comments

Comments

@dsedivec
Copy link

Hi! Thanks for making Tyro! I'd like to use it, but I can't figure out how to do mutually exclusive options. I've searched the docs and issues and I haven't turned anything up. Is it possible to make mutually exclusive options with Tyro?

@brentyi
Copy link
Owner

brentyi commented Feb 26, 2024

Hi!

We don't support this right now, primarily because there's no standard Python syntax that this behavior would correspond to.

If you feel there's a clean way to integrate this I'd be open to suggestions! We could, as an example, add something to tyro.conf or tyro.conf.arg().

@dsedivec
Copy link
Author

dsedivec commented Mar 1, 2024

We don't support this right now, primarily because there's no standard Python syntax that this behavior would correspond to.

If you feel there's a clean way to integrate this I'd be open to suggestions! We could, as an example, add something to tyro.conf o tyro.conf.arg().

OK, thanks! IMHO they're an essential part of argument parsing, so they're worth adding even at the cost of adding something clunky. I use them often enough in argparse that I consider it a downgrade to switch to an argument parsing library that lacks support for them (and most do lack support for them).

All that said... I don't really have a great suggestion even for something clunky. Click doesn't really have these, but Cloup sits on top of Click and implements it by adding "constraints" to option groups. I think the closest Tyro gets to groups is probably "hierarchical configs", and I take a strong moral position against option names with . (dot) in them. 😉

Typer doesn't really implement these either, though I believe the author suggested using a validator to enforce mutual exclusivity. You can't really inspect a validator method to produce your help message, though; you'd be unable to show the mutually exclusive options as mutually exclusive in --help.

In C, if I was implementing my options in a struct, I'd probably define mutually exclusive arguments in a union. I can't think of how that really translates to Python, since C unions crucially have differently-named members—it starts to look a lot like hierarchical configs.

You could do it with subclasses, where you've got a base class with common options then subclasses, one per mutually exclusive option. Aside from being kind of annoying to work with, god help you if you want two or more sets of mutually exclusive options. I guess you could use multiple inheritance?

As you suggested, I was originally thinking along the lines of using tyro.conf.arg to group mutually exclusive options together, kind of like Cloup does with option groups and constraints. That does not translate to Python's type system, but it accomplishes the end goal of leaving Tyro able to both enforce mutual exclusivity, and also to document this in the --help.


Anyway, thank you for your fast answer! You may feel free close this now if you want, as my question has been answered. 🙂

@brentyi
Copy link
Owner

brentyi commented Mar 1, 2024

Thanks for your thoughts! I'll keep this issue open while I'm thinking about this.

tyro's likely not the best tool if you need this soon, but your C-style union note is actually a really interesting one; since we can already rename arguments with tyro.conf.arg(), semantics like this could make sense:

# (mockup, not a real runnable example)

from typing import Annotated

import tyro
from tyro.conf import arg

def main(
    mutually_exclusive: (
        Annotated[int, arg(name="foo")]
        | Annotated[str, arg(name="bar")]
    )
) -> None:
    ...

if __name__ == "__main__":
    tyro.cli(main)

where mutually_exclusive can take the value of either --foo INT or --bar STR, but not both.

Another option is to build on typing.TypedDict's total=False: we could have something like a tyro.conf.MutuallyExclusiveTypedDictKeys[] to communicate that only one key in the dictionary should be populated.

In either case, the question becomes whether the advantages here outweigh the complexity + maintenance effort. I'm not sure, since (as per the Typer suggestion) this kind of mutual exclusivity can also be added to the helptext + validated after the tyro.cli() call, it's just much worse UX.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants