38

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?

Zulan
  • 21,896
  • 6
  • 49
  • 109
Gareth John
  • 383
  • 3
  • 4

5 Answers5

71

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
Will Vousden
  • 32,488
  • 9
  • 84
  • 95
  • 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. – Justin Jan 31 '22 at 03:24
16

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
hotenov
  • 911
  • 1
  • 11
  • 17
  • 1
    Can you explain where `new_func` comes from? (Should it be `wrapper` instead?) Also, any reason to not use `@wraps`? – dthor Jun 03 '22 at 00:21
  • 1
    @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 :) – hotenov Jun 03 '22 at 08:05
  • Could you tell me which type is the return from `common_options` and `wrapper_common_options` ? – Cristiano May 12 '23 at 07:13
  • @Cristiano, Hi! They are both return type `` :) 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](https://github.com/pallets/click/blob/bf1a6d4956cbbbfd0a6f4dd6310c8110cf89f7fe/src/click/decorators.py#L229) (where `FC = t.TypeVar("FC", bound=t.Union[t.Callable[..., t.Any], Command])`). **NOTE:** I linked to `8.0.x` branch (not latest) – hotenov May 12 '23 at 16:08
  • 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](https://click.palletsprojects.com/en/8.1.x/commands/#decorating-commands). – ryanjdillon Jun 23 '23 at 10:51
8

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
vzhd1701
  • 91
  • 1
  • 3
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
dothebart
  • 5,972
  • 16
  • 40
0

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
Rosa Gold
  • 1
  • 1