Python Click: access option values globally
Asked Answered
A

2

6

Say I have an flag --debug/--no-debug defined for the base command. This flag will affect the behavior of many operations in my program. Right now I find myself passing this flag as function parameters all over the place, which doesn't seem elegant. Especially when I need to access this flag in a deep call stack, I'll have to add this parameter to every single function on the stack.

I can instead create a global variable is_debug and set its value at the beginning of the command function that receives the value of this flag. But this doesn't seem elegant to me either.

Is there a better way to make some option values globally accessible using the Click library?

Abbey answered 16/10, 2020 at 0:5 Comment(0)
M
7

There are two ways to do so, depending on your needs. Both of them end up using the click Context.

Personally, I'm a fan of Option 2 because then I don't have to modify function signatures (and I rarely write multi-threaded programs). It also sounds more like what you're looking for.

Option 1: Pass the Context to the function

Use the click.pass_context decorator to pass the click context to the function.

Docs:

# test1.py
import click


@click.pass_context
def some_func(ctx, bar):
    foo = ctx.params["foo"]
    print(f"The value of foo is: {foo}")


@click.command()
@click.option("--foo")
@click.option("--bar")
def main(foo, bar):
    some_func(bar)


if __name__ == "__main__":
    main()
$ python test1.py --foo 1 --bar "bbb"
The value of foo is: 1

Option 2: click.get_current_context()

Pull the context directly from the current thread via click.get_current_context(). Available starting in Click 5.0.

Docs:

Note: This only works if you're in the current thread (the same thread as what was used to set up the click commands originally).

# test2.py
import click


def some_func(bar):
    c = click.get_current_context()
    foo = c.params["foo"]
    print(f"The value of foo is: {foo}")


@click.command()
@click.option("--foo")
@click.option("--bar")
def main(foo, bar):
    some_func(bar)


if __name__ == "__main__":
    main()
$ python test2.py --foo 1 --bar "bbb"
The value of foo is: 1
Mis answered 7/11, 2020 at 15:57 Comment(0)
A
0

To build on top of the Option 2 given by @dthor, I wanted to make this more seamless, so I combined it with the trick to modify global scope of a function and came up with the below decorator:

def with_click_params(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        g = func.__globals__
        sentinel = object()

        ctx = click.get_current_context()
        oldvalues = {}
        for param in ctx.params:
            oldvalues[param] = g.get(param, sentinel)
            g[param] = ctx.params[param]

        try:
            return func(*args, **kwargs)
        finally:
            for param in ctx.params:
                if oldvalues[param] is sentinel:
                    del g[param]
                else:
                    g[param] = oldvalues[param]

    return wrapper

You would use it like this (borrowing sample from @dthor's answer):

@with_click_params
def some_func():
    print(f"The value of foo is: {foo}")
    print(f"The value of bar is: {bar}")


@click.command()
@click.option("--foo")
@click.option("--bar")
def main(foo, bar):
    some_func()


if __name__ == "__main__":
    main()

Here is it in action:

$ python test2.py --foo 1 --bar "bbb"
The value of foo is: 1
The value of bar is: bbb

Caveats:

  • Function can only be called from a click originated call stack, but this is a conscious choice (i.e., you would make assumptions on the variable injection). The click unit testing guide should be useful here.
  • The function is no longer thread safe.

It is also possible to be explicit on the names of the params to inject:

def with_click_params(*params):
    def wrapper(func):
        @functools.wraps(func)
        def inner_wrapper(*args, **kwargs):
            g = func.__globals__
            sentinel = object()

            ctx = click.get_current_context()
            oldvalues = {}
            for param in params:
                oldvalues[param] = g.get(param, sentinel)
                g[param] = ctx.params[param]

            try:
                return func(*args, **kwargs)
            finally:
                for param in params:
                    if oldvalues[param] is sentinel:
                        del g[param]
                    else:
                        g[param] = oldvalue

        return inner_wrapper
    return wrapper


@with_click_params("foo")
def some_func():
    print(f"The value of foo is: {foo}")


@click.command()
@click.option("--foo")
@click.option("--bar")
def main(foo, bar):
    some_func()


if __name__ == "__main__":
    main()
Antonia answered 1/1, 2022 at 14:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.