Is it possible to reuse python @click.option decorators for multiple commands?
Asked Answered
C

5

40

I have two Python CLI tools which share a set of common click.options. At the moment, the common options are duplicated:

@click.command()
@click.option('--foo', is_flag=True)
@click.option('--bar', is_flag=True)
@click.option('--unique-flag-1', is_flag=True)
def command_one():
    pass

@click.command()
@click.option('--foo', is_flag=True)
@click.option('--bar', is_flag=True)
@click.option('--unique-flag-2', is_flag=True)
def command_two():
    pass

Is it possible to extract the common options in to a single decorator that can be applied to each function?

Counterspy answered 27/4, 2018 at 11:22 Comment(0)
Z
73

You can build your own decorator that encapsulates the common options:

def common_options(function):
    function = click.option('--unique-flag-1', is_flag=True)(function)
    function = click.option('--bar', is_flag=True)(function)
    function = click.option('--foo', is_flag=True)(function)
    return function

@click.command()
@common_options
def command():
    pass
Zolazoldi answered 27/4, 2018 at 11:31 Comment(1)
This is a good solution in many cases. An important thing to note is that the options and arguments are evaluated in reverse order. So, if you have something like my_tool say greeting name where greeting and name are arguments you would want to make sure that in your function you put the name argument before the greeting argument in the list.Ballast
S
17

And if you want to preserve click's option decorator syntax, you can implement your decorator in this way:

import functools

def common_options(f):
    @click.option('--foo', is_flag=True)
    @click.option('--bar', is_flag=True)
    @functools.wraps(f)
    def wrapper_common_options(*args, **kwargs):
        return f(*args, **kwargs)

    return wrapper_common_options


@click.command()
@common_options
@click.option('--unique-flag-1', is_flag=True)
def command_one():
    pass
Scythia answered 25/1, 2022 at 16:31 Comment(5)
Can you explain where new_func comes from? (Should it be wrapper instead?) Also, any reason to not use @wraps?Hauptmann
@Hauptmann 1) Yes, this was 'copy-paste' mistake. 2) Of course wraps is better. And my final code (in my project) was replaced with wraps(). But I forgot to change here. Sorry and thank you pointing out the error. I've edited my answer. Should be correct now :)Scythia
Could you tell me which type is the return from common_options and wrapper_common_options ?Clipclop
@Cristiano, Hi! They are both return type <class 'function'> :) But if you meant what type annotations we should write. Well, typing is not my strong point [yet], so I wrote just -> Callable[..., Any]: for common_options's return type. But you always can spent more time and adapt original @click.option decorator types. Look at their source code (where FC = t.TypeVar("FC", bound=t.Union[t.Callable[..., t.Any], Command])). NOTE: I linked to 8.0.x branch (not latest)Scythia
For those interested, there is an example using a method without a decorator that better illustrates the inner workings of this approach in the click docs.Laval
H
9

Here is a decorator that uses the same principle from the previous answer:

def group_options(*options):
    def wrapper(function):
        for option in reversed(options):
            function = option(function)
        return function
    return wrapper

opt_1 = click.option("--example1")
opt_2 = click.option("--example2")
opt_3 = click.option("--example3")

@cli.command()
@click.option("--example0")
@group_options(opt_1, opt_2, opt_3)
def command(example0, example1, example2, example3):
    pass
Heliotype answered 17/4, 2021 at 12:32 Comment(0)
L
1

If you want to add parameters to such a function, you need to wrap it once more:

def common_options(mydefault=True):
    def inner_func(function):
        function = click.option('--unique-flag-1', is_flag=True)(function)
        function = click.option('--bar', is_flag=True)(function)
        function = click.option('--foo', is_flag=True, default=mydefault)(function)
        return function
    return inner_func

@click.command()
@common_options(mydefault=False)
def command():
    pass
Larghetto answered 28/5, 2021 at 13:46 Comment(0)
G
1

For single reusable options, i just store the decorator returned by click.option in a variable. For multiple common options, i like to use a simple composition as a helper..

import functools

compose = lambda *funcs: functools.reduce(lambda f, g: lambda x: f(g(x)), funcs)

# reusable single option or argument
spam = click.option("--spam") 

# reusable group
foo_bar_baz = compose(
    click.option("--foo", is_flag=True),
    click.option("--bar", is_flag=True),
    click.option("--baz", is_flag=True),
)

@click.command()
@foo_bar_baz
@spam
def command1(foo, bar, baz, spam):
    pass

@click.command()
@foo_bar_baz
@spam
@click.option('--unique-flag', is_flag=True)
def command2(foo, bar, baz, spam, unique_flag):
    pass
Glairy answered 13/6, 2023 at 10:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.