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