Prohibit passing several feature switches in python click
Asked Answered
H

1

6

Python click allows specifying some command line options as "feature switches". Example from the official documentation:

@click.command()
@click.option('--upper', 'transformation', flag_value='upper',
              default=True)
@click.option('--lower', 'transformation', flag_value='lower')
def info(transformation):
    click.echo(getattr(sys.platform, transformation)())

As the name suggests, it is used for the case when there is several alternatives for some feature, and only one can be selected. The code above allows running the script as

$ test.py --upper
LINUX2
$ test.py --lower
linux2
$ test.py
LINUX2

However, the same script allows the user to specify both options on the command line. Click will silently use the last option specified:

$ test.py --upper --lower
linux2

Is there any way to force click to check that no more than one such option has been passed on the command line?

Homograph answered 20/3, 2018 at 15:10 Comment(0)
S
5

One way to approach what you are after is to inherit from click.Option, and customize the parser.

Custom Class:

import click


class OnceSameNameOption(click.Option):

    def add_to_parser(self, parser, ctx):

        def parser_process(value, state):
            # method to hook to the parser.process
            if self.name in state.opts:
                param_same_name = [
                    opt.opts[0] for opt in ctx.command.params
                    if isinstance(opt, OnceSameNameOption) and opt.name == self.name
                ]

                raise click.UsageError(
                    "Illegal usage: `{}` are mutually exclusive arguments.".format(
                        ', '.join(param_same_name))
                )

            # call the actual process
            self._previous_parser_process(value, state)

        retval = super(OnceSameNameOption, self).add_to_parser(parser, ctx)
        for name in self.opts:
            our_parser = parser._long_opt.get(name) or parser._short_opt.get(name)
            if our_parser:
                self._previous_parser_process = our_parser.process
                our_parser.process = parser_process
                break
        return retval

Using Custom Class:

To use the custom class, pass the cls parameter to @click.option() decorator like:

@click.option("--an_option", 'option-name', cls=OnceSameNameOption)

The string option-name is used to check for other invocations of the same option.

How does this work?

This works because click is a well designed OO framework. The @click.option() decorator usually instantiates a click.Option object but allows this behavior to be over ridden with the cls parameter. So it is a relatively easy matter to inherit from click.Option in our own class and over ride the desired methods.

In this case we over ride click.Option.add_to_parser() and the monkey patch the parser so that we can validate that the same name parameter has not been seen before.

Test Code:

@click.command()
@click.option('--upper', 'transformation', flag_value='upper',
              cls=OnceSameNameOption, default=True)
@click.option('--lower', 'transformation', flag_value='lower',
              cls=OnceSameNameOption)
def info(transformation):
    """Show the transformed platform"""
    click.echo(getattr(sys.platform, transformation)())

if __name__ == "__main__":
    commands = (
        '--upper --lower',
        '--upper',
        '--lower',
        '',
        '--help',
    )

    import sys, time

    time.sleep(1)
    print('Click Version: {}'.format(click.__version__))
    print('Python Version: {}'.format(sys.version))
    for cmd in commands:
        try:
            time.sleep(0.1)
            print('-----------')
            print('> ' + cmd)
            time.sleep(0.1)
            info(cmd.split())

        except BaseException as exc:
            if str(exc) != '0' and \
                    not isinstance(exc, (click.ClickException, SystemExit)):
                raise

Test Results:

Click Version: 6.7
Python Version: 3.6.2 (default, Jul 17 2017, 23:14:31) 
[GCC 5.4.0 20160609]
-----------
> --upper --lower
Error: Illegal usage: `--upper, --lower` are mutually exclusive arguments.
-----------
> --upper
LINUX
-----------
> --lower
linux
-----------
> 
LINUX
-----------
> --help
Usage: test.py [OPTIONS]

  Show the transformed platform

Options:
  --upper
  --lower
  --help   Show this message and exit.
Spinode answered 20/3, 2018 at 17:47 Comment(1)
@Petr, general solutions as usually more useful over time. Hopefully others will find it useful. Cheers.Spinode

© 2022 - 2024 — McMap. All rights reserved.