43

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?

5 Answers 5

76

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
Sign up to request clarification or add additional context in comments.

1 Comment

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.
20

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

5 Comments

Can you explain where new_func comes from? (Should it be wrapper instead?) Also, any reason to not use @wraps?
@dthor 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 :)
Could you tell me which type is the return from common_options and wrapper_common_options ?
@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)
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.
10

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

Comments

2

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

Comments

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.