Search code examples
pythonpython-click

Can click decorators work nicely on non-main functions?


I would like to have some CLI options to be re-used between two programs that share a common library file. Currently, each file has a similar structure to the following:

from .shared import G

@click.command()
@click.option("--thing1", …)
@click.optoin("--thing2", …)
…
def main(**kwargs):
    g = G()
    g.do_your_thing()

What I'd like to do is move all that to the __init__ of G or a subclass of G.

class G:
    @click.command()
    @click.option("--thing1")
    def __init__(**kwargs):
        pass

class G6(G):
    @click.command()
    @click.option("--specific-flag")
    def __init__(**kwargs):
        super().__init__(**kwargs)
        if kwargs.get("--specific-flag"):
             do_something()
        self.seven = 7

However, when I try this, I get the following error.

Usage: something.py [OPTIONS]
Try 'something.py --help' for help.

Error: no such option: --thing1

My primary goal with this refactoring is to eliminate lots of duplicative code decorating the main function of each program by moving the shared decorators to the library.

If this is possible, is it also possible to use an option decorator of the subclass to supersede the same decorator on the base class? I assume the answer is yes.


Edit

Based on the answer below, my program now works with a structure similar to the following snippet:

_common_options = [
    click.option(…), click.option(…), …
]

def common_options(func):
    for option in _common_options:
        func = option(func)
    return func

class G:
    def __init__(**kwargs):
        print(kwargs)

@click.command()
@common_options
@click.option("--specific-option", …)
def main(**kwargs):
    f = G(**kwargs)

The problem with my original attempt was putting all the @click.whatever stuff as a decorator of __init__ instead of decorating main and passing **kwargs to __init__.


Solution

  • What I've done previously was create a list of options to be applied to a command or group. For example:

    
    _global_options = [
        click.option('-v', '--verbose', count=True, default=0, help='Verbose output.'),
        click.option('-n', '--dry-run', is_flag=True, default=False, help='Dry-run mode.')
    ]
    
    
    def common_options(func):
        for option in reversed(_global_options):
            func = option(func)
        return func
    

    These would then be applied as such:

    click.group(name='my_group')
    @common_options
    def my_group(*args, **kwargs):
        pass
    

    Decorators seem to work really well with Click. Does this help?