Search code examples
pythonpython-3.xpython-click

How to define common python-click options for multiple commands?


In python 3.8 I want to define some click options that are common to multiple commands. I tried the following piece of code:

import click


@click.group()
@click.option(
    "-v",
    "--verbose",
    count=True,
    default=0,
    help="-v for DEBUG",
)
@click.option(
    "--path",
    help="Main Path.",
)
def cli():
  pass


@click.command("list")
@click.option(
    "--list-option",
    help="Special option for list command.",
)
def my_list_command(verbose, path, list_option):
    print(verbose, path, list_option)

@click.command("find")
@click.option(
    "--find-option",
    help="Special option for find command.",
)
def my_find_command(verbose, path, find_option):
    print(verbose, path, find_option)

cli.add_command(my_list_command)
cli.add_command(my_find_command)

if __name__ == '__main__':
    cli()

But when I try to run the command

python script.py list

I get an error

TypeError: cli() got an unexpected keyword argument 'verbose'

What I want, is that the command list has the following three options: verbose, path and list-option and that the command find has the following three options: verbose, path and find-option. I do not want to define the options for verbose and path twice.

Is there a way to do this?

I also tried to use @click.pass_context but that does not seem to solev the issue.


Solution

  • The way you currently defined it it will work, but the --verbose option belongs to the main command group, so you'd need to call it as python script.py --verbose list (and my_find_command and my_list_command won't receive it as an argument, only cli).

    To use the same option across multiple commands without repeating yourself too much, you can just assign it to a variable and then use it twice:

    verbose_option = click.option(
        "-v",
        "--verbose",
        count=True,
        default=0,
        help="-v for DEBUG",
    )
    
    ...
    
    @click.command()
    @verbose_option
    def foo(verbose):
        ...
    
    @click.command()
    @verbose_option
    def bar(verbose):
        ...
    

    Unrelated, but while we're at it: There's a simpler way of grouping commands, without having to do cli.add_command(my_find_command): Just use @cli.command() instead of @click.command():

    import click
    
    option_verbose = click.option(
        "-v",
        "--verbose",
        count=True,
        default=0,
        help="-v for DEBUG",
    )
    
    @click.group()
    def cli():
        pass
    
    
    @cli.command("list")
    @option_verbose
    @click.option(
        "--list-option",
        help="Special option for list command.",
    )
    def my_list_command(verbose, list_option):
        print(verbose, list_option)
    
    @cli.command("find")
    @option_verbose
    @click.option(
        "--find-option",
        help="Special option for find command.",
    )
    def my_find_command(verbose, list_option):
        print(verbose, list_option)
    
    if __name__ == '__main__':
        cli()
    

    If there's several options you want to apply, you can define your own decorator that calls all those options on the argument:

    def common_options(fn):
        return click.option(
            "-v",
            "--verbose",
        )(
            click.option(
                "-n",
                "--dry-run",
            )(fn)
        )