How can I create a mutually exclusive option group in Click? I want to either accept the flag "--all" or take an option with a parameter like "--color red".
Click, or “Command Line Interface Creation Kit” is a Python library for building command line interfaces. The three main points of Python Click are arbitrary nesting of commands, automatic help page generation, and supporting lazy loading of subcommands at runtime.
This is a special attribute where commands are supposed to remember what they need to pass on to their children. In order for this to work, we need to mark our function with pass_context() , because otherwise, the context object would be entirely hidden from us.
Click supports two types of parameters for scripts: options and arguments. There is generally some confusion among authors of command line scripts of when to use which, so here is a quick overview of the differences. As its name indicates, an option is optional.
I ran into this same use case recently; this is what I came up with. For each option, you can give a list of conflicting options.
from click import command, option, Option, UsageError
class MutuallyExclusiveOption(Option):
def __init__(self, *args, **kwargs):
self.mutually_exclusive = set(kwargs.pop('mutually_exclusive', []))
help = kwargs.get('help', '')
if self.mutually_exclusive:
ex_str = ', '.join(self.mutually_exclusive)
kwargs['help'] = help + (
' NOTE: This argument is mutually exclusive with '
' arguments: [' + ex_str + '].'
)
super(MutuallyExclusiveOption, self).__init__(*args, **kwargs)
def handle_parse_result(self, ctx, opts, args):
if self.mutually_exclusive.intersection(opts) and self.name in opts:
raise UsageError(
"Illegal usage: `{}` is mutually exclusive with "
"arguments `{}`.".format(
self.name,
', '.join(self.mutually_exclusive)
)
)
return super(MutuallyExclusiveOption, self).handle_parse_result(
ctx,
opts,
args
)
Then use the regular option
decorator but pass the cls
argument:
@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="The jar file the topology lives in.",
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)
if __name__ == '__main__':
cli()
Here's a gist that includes the code above and shows the output from running it.
If that won't work for you, there's also a few (closed) issues mentioning this on the click github page with a couple of ideas that you may be able to use.
You could use the following package: https://github.com/espdev/click-option-group
import click
from click_option_group import optgroup, RequiredMutuallyExclusiveOptionGroup
@click.command()
@optgroup.group('Grouped options', cls=RequiredMutuallyExclusiveOptionGroup,
help='Group description')
@optgroup.option('--all', 'all_', is_flag=True, default=False)
@optgroup.option('--color')
def cli(all_, color):
print(all_, color)
if __name__ == '__main__':
cli()
app help:
$ app.py --help
Usage: app.py [OPTIONS]
Options:
Grouped options: [mutually_exclusive, required]
Group description
--all
--color TEXT
--help Show this message and exit.
You could use Cloup, a package that adds option groups and constraints to Click. You have two options to solve this problem in Cloup.
Disclaimer: I'm the author of the package.
When you define an option group using @option_group
, the options in each group are shown in separate help sections (like in argparse). You can apply constraints (like mutually_exclusive
) to option groups as follows:
from cloup import command, option, option_group
from cloup.constraints import mutually_exclusive
@command()
@option_group(
'Color options',
option('--all', 'all_colors', is_flag=True),
option('--color'),
constraint=mutually_exclusive
)
def cmd(**kwargs):
print(kwargs)
The help will be:
Usage: cmd [OPTIONS]
Color options [mutually exclusive]:
--all
--color TEXT
Other options:
--help Show this message and exit.
If you don't want option groups to show up in the command help, you can use @constraint
and specify the constrained options by their (destination) name:
from cloup import command, option
from cloup.constraints import constraint, mutually_exclusive
@command()
@option('--all', 'all_colors', is_flag=True)
@option('--color')
@constraint(mutually_exclusive, ['all_colors', 'color'])
def cmd(**kwargs):
print(kwargs)
Constraints defined this way can be documented in command help! This feature is disabled by default but can be easily enabled passing show_constraints=True
to @command
. The result:
Usage: cmd [OPTIONS]
Options:
--all
--color TEXT
--help Show this message and exit.
Constraints:
{--all, --color} mutually exclusive
UPDATE: it's now possible to use constraints as decorators rather than using @contraint
:
@command()
@mutually_exclusive(
option('--all', 'all_colors', is_flag=True),
option('--color'),
)
def cmd(**kwargs):
print(kwargs)
In both cases, if you run cmd --all --color red
, you get:
Usage: cmd [OPTIONS]
Try 'cmd --help' for help.
Error: the following parameters are mutually exclusive:
--all
--color
Cloup defines constraints that should cover 99.9% of your needs. It even supports conditional constraints!
For example, if the user must provide one of your mutually exclusive options, replace mutually_exclusive
with RequireExactly(1)
in the example above.
You can find all available constraints here.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With