Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mutually exclusive option groups in python Click

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

like image 501
b-jazz Avatar asked May 18 '16 21:05

b-jazz


People also ask

What is Click option in Python?

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.

What is click Pass_context?

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.

Which of the following is parameter of the click command?

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.


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

  • https://github.com/pallets/click/issues/257
  • https://github.com/pallets/click/issues/509
like image 57
Jacob Avatar answered Nov 15 '22 03:11

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.
like image 43
iroln Avatar answered Nov 15 '22 02:11

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.

like image 7
janluke Avatar answered Nov 15 '22 02:11

janluke