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?

2 Answers 2

8

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
Sign up to request clarification or add additional context in comments.

Comments

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()

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.