Chen Shabi
Chen Shabi

Reputation: 174

Is it possible to pass a click option at the end of the entire command?

Is it possible to set a click.option on a click.group but pass the option in the end of the entire command?

@click.option(
    '-d', '--debug',
    help="change to debug mode")
@click.group()
def cli(ctx, **kwargs):
    """
    help message
    """

@click.command()
def version():
    """
    version
    """
    pass

And the desired command would look like this:

./cli.py version -d 

But this fails with:

Error: no such option: -d

Upvotes: 1

Views: 2125

Answers (2)

Stephen Rauch
Stephen Rauch

Reputation: 49794

One way to to set a click.option on a click.group but pass the option in the end of the entire command is to inherit from click.Group, and customize the code which invokes the commands.

This question was partially answered here. From that question we will use the custom class: GroupWithCommandOptions.

Custom Class:

import click

class GroupWithCommandOptions(click.Group):
    """ Allow application of options to group with multi command """

    def add_command(self, cmd, name=None):
        click.Group.add_command(self, cmd, name=name)

        # add the group parameters to the command
        for param in self.params:
            cmd.params.append(param)

        # hook the commands invoke with our own
        cmd.invoke = self.build_command_invoke(cmd.invoke)
        self.invoke_without_command = True

    def build_command_invoke(self, original_invoke):

        def command_invoke(ctx):
            """ insert invocation of group function """

            # separate the group parameters
            ctx.obj = dict(_params=dict())
            for param in self.params:
                name = param.name
                ctx.obj['_params'][name] = ctx.params[name]
                del ctx.params[name]

            # call the group function with its parameters
            params = ctx.params
            ctx.params = ctx.obj['_params']
            self.invoke(ctx)
            ctx.params = params

            # now call the original invoke (the command)
            original_invoke(ctx)

        return command_invoke

Using Custom Class:

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

@click.group(cls=GroupWithCommandOptions)

Then create a click.CommandCollection using the group

cli = click.CommandCollection(sources=[group1])

The command collection is important because we need to control when the group is invoked, and CommandCollection's do not invoke their group, so we can do it ourselves at the appropriate time.

How does this work?

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

In this case we over ride click.Group.add_command() to add the group parameters to the command and then monkey patch the command's invoke() method so can call the group invoke before command invoke to allow it to process its options which were specified on the command.

Test Code:

@click.group(cls=GroupWithCommandOptions)
@click.option('-d', '--debug', help="change to debug mode", is_flag=True)
def group1(**kwargs):
    """Our great program"""
    click.echo('debug: %s' % kwargs['debug'])


@group1.command()
@click.pass_context
def version(ctx):
    """Show the Version"""
    click.echo('show version here')


cli = click.CommandCollection(sources=[group1])


if __name__ == "__main__":
    commands = (
        'version -d',
        '-d version',
        'version',
        '--help',
        'version --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)
            cli(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]
-----------
> version -d
debug: True
show version here
-----------
> -d version
Error: no such option: -d
-----------
> version
debug: False
show version here
-----------
> --help
Usage: test.py [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  version  Show the Version
-----------
> version --help
Usage: test.py version [OPTIONS]

  Show the Version

Options:
  -d, --debug  change to debug mode
  --help       Show this message and exit.

Upvotes: 2

Chen Shabi
Chen Shabi

Reputation: 174

Achieving this using click is impossible as an option belongs to it's command. An option that is available to all commands should belong to the group that encloses the commands.

A couple of implementation examples for achieving that can be found here: https://github.com/pallets/click/issues/108

Upvotes: -1

Related Questions