Mutually exclusive option groups in python Click

Question:

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”.

Asked By: b-jazz

||

Answers:

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.

Answered By: Jacob

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.
Answered By: iroln

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.

Option 1: @option_group

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.

Option 2: apply the constraint without defining an option group

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)

The error message

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

Other constraints

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.

Answered By: janluke
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.