Search code examples
pythonpython-click

Adding common parameters to groups with Click


I am trying to use the Python library Click, but struggle to get an example working. I defined two groups, one of which (group2) is meant to handle common parameters for this group of commands. What I want to achieve is that those common parameters get processed by the group function (group2) and assigned to the context variable, so they can be used by the actual commands.

A use case would be a number of commands that require username and password, while some others don't (not even optionally).

This is the code

import click


@click.group()
@click.pass_context
def group1(ctx):
    pass


@click.group()
@click.option('--optparam', default=None, type=str)
@click.option('--optparam2', default=None, type=str)
@click.pass_context
def group2(ctx, optparam):
    print 'in group2', optparam
    ctx['foo'] = create_foo_by_processing_params(optparam, optparam2)


@group2.command()
@click.pass_context
def command2a(ctx):
    print 'command2a', ctx['foo']


@group2.command()
@click.option('--another-param', default=None, type=str)
@click.pass_context
def command2b(ctx, another_param):
    print 'command2b', ctx['foo'], another_param

# many more more commands here...
# @group2.command()
# def command2x():
# ...


@group1.command()
@click.argument('argument1')
@click.option('--option1')
def command1(argument1, option1):
    print 'In command2', argument1, option1

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


if __name__ == '__main__':
    cli(obj={})

And this is the result when using command2:

$ python cli-test.py command2 --optparam=123
> Error: no such option: --optparam`

What's wrong with this example. I tried to follow the docs closely, but opt-param doesn't seem to be recognised.


Solution

  • The basic issue with the desired scheme is that click.CommandCollection does not call the group function. It skips directly to the command. In addition it is desired to apply options to the group via decorator, but have the options parsed by the command. That is:

    > my_prog my_command --group-option
    

    instead of:

    > my_prog --group-option my_command
    

    How?

    This click.Group derived class hooks the command invocation for the commands to intercept the group parameters, and pass them to the group command.

    1. In Group.add_command, add the params to the command
    2. In Group.add_command, override command.invoke
    3. In overridden command.invoke, take the special args inserted from the group and put them into ctx.obj and remove them from params
    4. In overridden command.invoke, invoke the group command, and then the command itself

    Code:

    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
    

    Test Code:

    @click.group()
    @click.pass_context
    def group1(ctx):
        pass
    
    @group1.command()
    @click.argument('argument1')
    @click.option('--option1')
    def command1(argument1, option1):
        click.echo('In command2 %s %s' % (argument1, option1))
    
    
    @click.group(cls=GroupWithCommandOptions)
    @click.option('--optparam', default=None, type=str)
    @click.option('--optparam2', default=None, type=str)
    @click.pass_context
    def group2(ctx, optparam, optparam2):
        # create_foo_by_processing_params(optparam, optparam2)
        ctx.obj['foo'] = 'from group2 %s %s' % (optparam, optparam2)
    
    @group2.command()
    @click.pass_context
    def command2a(ctx):
        click.echo('command2a foo:%s' % ctx.obj['foo'])
    
    @group2.command()
    @click.option('--another-param', default=None, type=str)
    @click.pass_context
    def command2b(ctx, another_param):
        click.echo('command2b %s %s' % (ctx['foo'], another_param))
    
    cli = click.CommandCollection(sources=[group1, group2])
    
    if __name__ == '__main__':
        cli('command2a --optparam OP'.split())
    

    Results:

    command2a foo:from group2 OP None