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

Ability to create mutually exclusive option groups. #257

Closed
robhague opened this issue Nov 24, 2014 · 37 comments
Closed

Ability to create mutually exclusive option groups. #257

robhague opened this issue Nov 24, 2014 · 37 comments

Comments

@robhague
Copy link

Perhaps I'm missing something, but I can't find a way to create mutually exclusive option groups in Click. For example, I have a command that can take input from either a socket or a file, and I'd like the following syntax:

cmd [-file <filename> | -stream <address>] ...

At most one (or, in other examples, exactly one) of the options may be specified. Specifying more than one is a parse error caught by the library.

Is it possible to implement this in click at present? If not, would it be feasible to add it? I've not quite got my head around the general architecture yet. Would this feature be a reasonable fit, or does it go against the overall design in some way I've missed?

@hartror
Copy link

hartror commented Feb 24, 2015

This is something I'd like as well which is currently not supported.

Perhaps it could look something like this:

@click.command()
@click.option_group(Option('-file'), Option('-stream'))
def command(file=None, stream=None):
    pass

Or perhaps option group could allow for mutally inclusive options, i.e -foo must be called with -bar. Inclusive seems the sensible default so there could be a param for exclusive such as:

@click.option_group(Option('-file'), Option('-stream'), exclusive=True)

@mitsuhiko thoughts? I'd be happy to put together a PR for this feature.

@ryanneufeld
Copy link

+1

@mitsuhiko
Copy link
Contributor

I don't really see how this works from an API point of view. I would rather see some sort of post validation utility that can check that only one is set or something.

Definitely a strong -1 on the option_group thing.

@hartror
Copy link

hartror commented Mar 31, 2015

post validation utility

Not clear on what you mean @mitsuhiko, "utility" in the context of click appears to mean methods like click.echo? If this is the case how would this allow click to generate a useful help message on --help?

More than happy for you to suggest an API for me to go implement and submit a PR.

@mitsuhiko
Copy link
Contributor

Not clear on what you mean @mitsuhiko, "utility" in the context of click appears to mean methods like click.echo? If this is the case how would this allow click to generate a useful help message on --help?

Pretty much yes. I don't see how click would generate a nice looking help page for such things anyways. This gets too complex quickly. For instance some options might only conflict in relation to a specific setting of another option.

@nchammas
Copy link
Contributor

nchammas commented May 6, 2015

I'm with @mitsuhiko in that I'm not sure how this would work in Click, but for what it's worth I have a similar use case.

The full use case is described in #329, but more simply: I have a tool that launches stuff into various cloud providers like AWS, GCE, and so forth. The choice of provider is an option.

Depending on what --provider is chosen, additional provider-specific options are required.

Personally, I don't care that the help page capture these kinds of dependencies between options. (e.g. If --provider is set to ec2, then --ec2-region must be specified.)

I care more that the dependency be captured in the parser (or perhaps in some nice utility method) and that helpful error message be provided.

My use case is quite complex, though. Perhaps it is a symptom of a complex design that Click shouldn't try to support.

ashander added a commit to ashander/sumatra that referenced this issue May 8, 2015
Complete overhaul of top-level parsing.  Delegation to subcommands using
click.group(). Structure of parsing within each of the subcommand is
retained, but some of the wrapping code for help functions and version
options is no longer needed.

Changes to CLI API:

- webdav/archive/mirror flags no longer specifiable as mutually
  exclusive group
- argument order in comment is changed to smt comment COMMENT [LABEL]

The first of these cannot be fixed without changes to click API (see
issue open-research#257 pallets/click#257). The second is
a little more annoying from a user perspective. Though it may be fixable
given current api. So far, though, I can't figure out how to pass
optional arguments before required. I wouldn't be surprised if it isn't
possible either.

A few associated changes:

* Currently using click to print in simplest way possible: click echo,
* and for
  error-like messages specifying err=True to print to standard error.
* Because of change, help files look and print slightly (or in some
  cases majorly) differently

Changes to commands.py API:

* Breaking changes to the arguments expected by all functions
  implementing smt subcommands. This is unavoidable with click.

Remaining issues :

* many test failures due to API changes -- around 67
  (filtering calls to parse_arguments from grep -E 'commands\..+(\(|\,)'
  test_commands.py) lines of the test suite for commands.py will need to
  be rewritten
* test rewrite should be straightforward change from tuples to named args
* migration to click doesn't intend to change downstream behavior so
  passing tests can serve as a check on rewrite
* docs not updated to modified look of help pages
@alanhamlett
Copy link

@nchammas have you found a solution for dynamically making --ec2-region a required option?

@nchammas
Copy link
Contributor

nchammas commented Jun 9, 2015

@alanhamlett - Not yet, but when I have the time I am planning to write a utility function that captures simple dependencies like this. If it works well for my use case I will post it here as a proposed solution to this issue.

@alanhamlett
Copy link

I just used a callback function for now:
wakatime/wakadump@fc88de6

@alexandre-mbm
Copy link

👍

@mitsuhiko
Copy link
Contributor

I'm going to close this. There is no likely API for this and you can already manually check on this in callbacks.

@alexandre-mbm
Copy link

you can already manually check on this in callbacks

I don't know if we are talking about the same thing, I don't understand as I use callback functions to create mutually exclusive option groups, and I am not familiar with the click's code, but I have rehearsed the following code for my project:

https://github.com/OSMBrasil/paicemana/blob/master/paicemana/clickexclusive.py

@untitaker
Copy link
Contributor

You can store data on ctx.obj in one callback and check in the other callback if that data is there. If it is there, the user has used both options and you can raise an error to enforce the exclusivity of these two options.

@alexandre-mbm
Copy link

The programation of the callback functions can become very complicated with many parameters. I am in favor of something using annotations, but I can not to perform the changes.

@untitaker
Copy link
Contributor

I don't think it's complicated at all.

@alexandre-mbm
Copy link

How would be an example for cli.py command -a|-b|-c|-d|-e|-f? No need to show it if it is not simple.

@untitaker
Copy link
Contributor

Are you talking about the problematic formatting of such options in the help page?

Why not use an option of type Choice?

On 23 July 2015 17:59:10 CEST, Alexandre Magno notifications@github.com wrote:

How would be an example for cli.py command -a|-b|-c|-d|-e|-f? No need
to show it if it is not simple.


Reply to this email directly or view it on GitHub:
#257 (comment)

Sent from my phone. Please excuse my brevity.

@alexandre-mbm
Copy link

Parameters of different meanings, names, types and requirements.

@untitaker
Copy link
Contributor

Do you know of any pracical example that features so many mutually exclusive parameters with different types?

On 23 July 2015 18:20:04 CEST, Alexandre Magno notifications@github.com wrote:

Parameters of different meanings, names and types.


Reply to this email directly or view it on GitHub:
#257 (comment)

Sent from my phone. Please excuse my brevity.

@alexandre-mbm
Copy link

OSMBrasil/paicemana#15. See the github-* commands.

@untitaker
Copy link
Contributor

I strongly suspect those should be implemented as subcommands, though I'm not really trusting Google Translate.

@alexandre-mbm
Copy link

Semantically I prefer to reserve the universe of commands for program expansion. Anyway, I think this is a decision very "personal" and that the click should not impose such restriction. But I know that I can use Choice options from refactoring...

Let's say I or other interest in studying and implementing something like the #257 (comment) example, would you be open to receive pull requests in this regard?

@untitaker
Copy link
Contributor

Semantically I prefer to reserve the universe of commands for program expansion.

I don't know what you mean by that sentence. I was thinking about a command-line like this:

  • Instead of paicemana osmf -g, do paicemana osmf get
  • Instead of paicemana osmf -p, do paicemana osmf put
  • Instead of paicemana osmf -s, do paicemana osmf sync

Anyway, I think this is a decision very "personal" and that the click should not impose such restriction.

Click imposes such restrictions all the time. Limiting ones "personal freedoms" is Click's spirit, so to speak, to achieve a certain consistency across command-line applications.

@alexandre-mbm
Copy link

Sorry, I had not given me of that paicemana osmf get was possible. I'll get to learn it. Yes, this Click's spirit is appropriate. Thanks.

@untitaker
Copy link
Contributor

Like this:

@click.group()
def paicemana():
    pass

@paicemana.group()
def osmf():
    pass

@osmf.command()
def put():
    pass

@alexandre-mbm
Copy link

Oh! This is very good. Thank you very much. I haven't found this at documentation.


Update

Now I am reading here:

@alexandre-mbm
Copy link

@untitaker, is possible to show a full help (commands and subcommands)?

@untitaker
Copy link
Contributor

No, not at the moment, feel free to open a new issue.

@nchammas
Copy link
Contributor

nchammas commented Jan 9, 2016

Quick update for people: I put together a few utility methods for my project Flintrock that enforce option dependencies like we've been discussing here.

The utility methods help you enforce the following kinds of requirements:

  • Option A requires all of options B, C, and D to be set.
  • Option A requires any of options B, C, or D to be set.
  • Option A requires all of options B, C, and D to be set, and any of options E, F, G to also be set.
  • Option A has the same requirements as in any of the previous examples, except that these requirements are conditional on Option A having a value of V.
  • Options A, B, and C are mutually exclusive. Only 1 of them can be set.

I've added these utility methods to my project in this PR: nchammas/flintrock#74

Here are a few example invocations to illustrate:

option_requires(
    option='--install-hdfs',
    requires_all=['--hdfs-version'],
    scope=locals())

option_requires(
    option='--install-spark',
    requires_any=[
        '--spark-version',
        '--spark-git-commit'],
    scope=locals())

mutually_exclusive(
    options=[
        '--spark-version',
        '--spark-git-commit'],
    scope=locals())

option_requires(
    option='--provider',
    conditional_value='ec2',
    requires_all=[
        '--ec2-key-name',
        '--ec2-identity-file',
        '--ec2-instance-type',
        '--ec2-region',
        '--ec2-ami',
        '--ec2-user'],
    scope=locals())

This is pretty hacky with the passing of locals() and the janky lookup of option values by converting from "option names" to "variable names", but the API is very readable.

I'm not proposing this for addition to Click -- I don't think this API would work for most people -- but I thought I'd share my work since others have been looking for a way to enforce these kinds of requirements.

@jacobtolar
Copy link
Contributor

I needed to do this too. Here's what I came up with; not perfect (for each option, you need to give the list of other conflicting options; there's no nice global definition), but it works well enough for what I need.

https://gist.github.com/jacobtolar/fb80d5552a9a9dfc32b12a829fa21c0c

Example usage:

@command(help="Run the command.")
@option('--jar-file', cls=MutuallyExclusiveOption, help="The jar file the topology lives in.", mutually_exclusive=["other_arg"])
@option('--other-arg', cls=MutuallyExclusiveOption, help="Another argument.", mutually_exclusive=["jar_file"])
def cli(jar_file, other_arg):
    print "Running cli."
    print "jar-file: {}".format(jar_file)
    print "other-arg: {}".format(other_arg)

@listx
Copy link

listx commented Oct 14, 2016

I think this topic should be addressed in the official docs (and state why Click does not support this feature), under the "Choice Options" (http://click.pocoo.org/6/options/#choice-options) heading, because the idea is very similar.

@listx
Copy link

listx commented Oct 14, 2016

@jacobtolar FWIW I made this revision that allows you to just define a single list of allowed choices for each option. https://gist.github.com/listx/e06c7561bddfe47346e41a23a3026f33

The usage remains identical, except instead of passing in disparate lists for each argument, you pass in the same list each time.

@thebopshoobop
Copy link

I just hacked together a quick and dirty, differently-styled solution to this problem, which makes it easy to specify multiple different exclusive relationships.
https://gist.github.com/thebopshoobop/51c4b6dce31017e797699030e3975dbf

@cultcom
Copy link

cultcom commented Dec 28, 2017

But what about options that are required depending on the value of another option?
Like for SNMP:
--version [ 1 | 2c | 3 ]
Depending on the value of "version" I need different further options:
"1 | 2c" require a "community string" only

"3" requires security context information and passphrases like -l -u -a -A -x -X

Any idea how to implement that with click?

@omniproc
Copy link

omniproc commented Jul 8, 2018

For finding this post and wondering if there is a better solution for simple mutex options here is my slightly changed version from https://stackoverflow.com/questions/44247099/click-command-line-interfaces-make-options-required-if-other-optional-option-is using a custom class to validate the options:

import click

class Mutex(click.Option):
    def __init__(self, *args, **kwargs):
        self.not_required_if:list = kwargs.pop("not_required_if")

        assert self.not_required_if, "'not_required_if' parameter required"
        kwargs["help"] = (kwargs.get("help", "") + "Option is mutually exclusive with " + ", ".join(self.not_required_if) + ".").strip()
        super(Mutex, self).__init__(*args, **kwargs)

    def handle_parse_result(self, ctx, opts, args):
        current_opt:bool = self.name in opts
        for mutex_opt in self.not_required_if:
            if mutex_opt in opts:
                if current_opt:
                    raise click.UsageError("Illegal usage: '" + str(self.name) + "' is mutually exclusive with " + str(mutex_opt) + ".")
                else:
                    self.prompt = None
        return super(Mutex, self).handle_parse_result(ctx, opts, args)

Use it like this:

@click.group()
@click.option("--username", prompt=True, cls=Mutex, not_required_if=["token"])
@click.option("--password", prompt=True, hide_input=True, cls=Mutex, not_required_if=["token"])
@click.option("--token", cls=Mutex, not_required_if=["username","password"])
def login(ctx=None, username:str=None, password:str=None, token:str=None) -> None:
	print("...do what you like with the params you got...")

@espdev
Copy link

espdev commented Dec 5, 2019

I have created the project: https://github.com/espdev/click-option-group
I would be glad if it is useful to someone. :)

@espdev
Copy link

espdev commented Dec 5, 2019

@mitsuhiko what do you think about such API?

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Nov 13, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests