decorate on top of a click command
Asked Answered
S

3

8

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.

Strenta answered 5/4, 2018 at 16:20 Comment(0)
C
4

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):
Chevrotain answered 5/4, 2018 at 16:36 Comment(1)
Thanks! I tested it, both work, is what I needed, especially the "which parameter" version.Strenta
A
11

Harvey's answer won't work with command groups. Effectively this would replace the 'hello' command with 'wrapper' which is not what we want. Instead try something like:

from functools import wraps

def test_decorator(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        kwargs['name'] = kwargs['name'].upper()
        return f(*args, **kwargs)
    return wrapper
Attenuate answered 17/10, 2018 at 15:40 Comment(2)
I am seeing the following error when I tried this with group: AttributeError: 'function' object has no attribute 'command'Harrus
In my case the command is converting to <class 'function'> even after using @wraps.Harrus
C
4

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):
Chevrotain answered 5/4, 2018 at 16:36 Comment(1)
Thanks! I tested it, both work, is what I needed, especially the "which parameter" version.Strenta
S
2

About click command groups - we need to take into account what the documentation says - https://click.palletsprojects.com/en/7.x/commands/#decorating-commands

So in the end a simple decorator like this:

def sample_decorator(f):
    def run(*args, **kwargs):
        return f(*args, param="yea", **kwargs)
    return run

needs to be converted to work with click:

from functools import update_wrapper

def sample_decorator(f):
    @click.pass_context
    def run(ctx, *args, **kwargs):
        return ctx.invoke(f, *args, param="yea", **kwargs)
    return update_wrapper(run, f)

(The documentation suggests using ctx.invoke(f, ctx.obj, but that has led to an error of 'duplicite arguments'.)

Sardanapalus answered 20/8, 2020 at 7:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.