Search code examples
pythonpython-3.xcommand-line-argumentspython-decoratorspython-click

decorate on top of a click command


I am trying to decorate a function which is already decorated by @click and called from the command line.

Normal decoration to capitalise the input could look like this:

standard_decoration.py

def capitalise_input(f):
    def wrapper(*args):
        args = (args[0].upper(),)
        f(*args)
    return wrapper

@capitalise_input
def print_something(name):
    print(name)

if __name__ == '__main__':
    print_something("Hello")

Then from the command line:

$ python standard_decoration.py
HELLO

The first example from the click documentation looks like this:

hello.py

import click

@click.command()
@click.option('--count', default=1, help='Number of greetings.')
@click.option('--name', prompt='Your name',
              help='The person to greet.')
def hello(count, name):
    """Simple program that greets NAME for a total of COUNT times."""
    for x in range(count):
        click.echo('Hello %s!' % name)

if __name__ == '__main__':
    hello()

When run from the command line:

$ python hello.py --count=3
Your name: John
Hello John!
Hello John!
Hello John!
  1. What is the correct way to apply a decorator which modifies the inputs to this click decorated function, eg make it upper-case just like the one above?

  2. Once a function is decorated by click, would it be true to say that any positional arguments it has are transformed to keyword arguments? It seems that it matches things like '--count' with strings in the argument function and then the order in the decorated function no longer seems to matter.


Solution

  • It appears that click passes keywords arguments. This should work. I think it needs to be the first decorator, i.e. it is called after all of the click methods are done.

    def capitalise_input(f):
        def wrapper(**kwargs):
            kwargs['name'] = kwargs['name'].upper()
            f(**kwargs)
        return wrapper
    
    
    @click.command()
    @click.option('--count', default=1, help='Number of greetings.')
    @click.option('--name', prompt='Your name',
                  help='The person to greet.')
    @capitalise_input
    def hello(count, name):
        ....
    

    You could also try something like this to be specific about which parameter to capitalize:

    def capitalise_input(key):
        def decorator(f):
            def wrapper(**kwargs):
                kwargs[key] = kwargs[key].upper()
                f(**kwargs)
            return wrapper
        return decorator
    
    @capitalise_input('name')
    def hello(count, name):