Python 点击:全局访问选项值
Python Click: access option values globally
假设我为基本命令定义了一个标志 --debug/--no-debug
。该标志将影响我程序中许多操作的行为。现在我发现自己到处都将这个标志作为函数参数传递,这看起来并不优雅。特别是当我需要在深度调用堆栈中访问此标志时,我必须将此参数添加到堆栈上的每个函数。
我可以改为创建一个全局变量 is_debug
并在接收此标志值的命令函数的开头设置它的值。但这对我来说也不优雅。
是否有更好的方法使用 Click 库使某些选项值可全局访问?
根据您的需要,有两种方法可以做到这一点。他们俩最终都使用了点击 Context.
就个人而言,我是选项 2 的粉丝,因为这样我就不必修改函数签名(而且我很少编写 multi-threaded 程序)。它也听起来更像您要找的东西。
选项 1:将上下文传递给函数
使用 click.pass_context
装饰器将点击上下文传递给函数。
文档:
- 用法:https://click.palletsprojects.com/en/7.x/commands/#nested-handling-and-contexts
- API: https://click.palletsprojects.com/en/7.x/api/#click.pass_context
# 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
选项 2:click.get_current_context()
通过click.get_current_context()
直接从当前线程中提取上下文。从 Click 5.0 开始可用。
文档:
- 用法:https://click.palletsprojects.com/en/7.x/advanced/#global-context-access
- API: https://click.palletsprojects.com/en/7.x/api/#click.get_current_context
注意:这仅在您处于当前线程(与最初用于设置点击命令的线程相同的线程)中时有效。
# 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
要在@dthor 给出的 选项 2 之上构建,我想让它更加无缝,所以我将它与 trick to modify global scope of a function 结合起来使用以下装饰器:
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
你会像这样使用它(从@dthor 的回答中借用样本):
@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()
这是实际操作:
$ python test2.py --foo 1 --bar "bbb"
The value of foo is: 1
The value of bar is: bbb
注意事项:
- 函数只能从
click
发起的调用堆栈调用,但这是一个有意识的选择(即,您会对变量注入做出假设)。 click unit testing guide 在这里应该有用。
- 该函数不再是线程安全的。
也可以明确指定要注入的参数的名称:
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()
假设我为基本命令定义了一个标志 --debug/--no-debug
。该标志将影响我程序中许多操作的行为。现在我发现自己到处都将这个标志作为函数参数传递,这看起来并不优雅。特别是当我需要在深度调用堆栈中访问此标志时,我必须将此参数添加到堆栈上的每个函数。
我可以改为创建一个全局变量 is_debug
并在接收此标志值的命令函数的开头设置它的值。但这对我来说也不优雅。
是否有更好的方法使用 Click 库使某些选项值可全局访问?
根据您的需要,有两种方法可以做到这一点。他们俩最终都使用了点击 Context.
就个人而言,我是选项 2 的粉丝,因为这样我就不必修改函数签名(而且我很少编写 multi-threaded 程序)。它也听起来更像您要找的东西。
选项 1:将上下文传递给函数
使用 click.pass_context
装饰器将点击上下文传递给函数。
文档:
- 用法:https://click.palletsprojects.com/en/7.x/commands/#nested-handling-and-contexts
- API: https://click.palletsprojects.com/en/7.x/api/#click.pass_context
# 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
选项 2:click.get_current_context()
通过click.get_current_context()
直接从当前线程中提取上下文。从 Click 5.0 开始可用。
文档:
- 用法:https://click.palletsprojects.com/en/7.x/advanced/#global-context-access
- API: https://click.palletsprojects.com/en/7.x/api/#click.get_current_context
注意:这仅在您处于当前线程(与最初用于设置点击命令的线程相同的线程)中时有效。
# 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
要在@dthor 给出的 选项 2 之上构建,我想让它更加无缝,所以我将它与 trick to modify global scope of a function 结合起来使用以下装饰器:
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
你会像这样使用它(从@dthor 的回答中借用样本):
@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()
这是实际操作:
$ python test2.py --foo 1 --bar "bbb"
The value of foo is: 1
The value of bar is: bbb
注意事项:
- 函数只能从
click
发起的调用堆栈调用,但这是一个有意识的选择(即,您会对变量注入做出假设)。 click unit testing guide 在这里应该有用。 - 该函数不再是线程安全的。
也可以明确指定要注入的参数的名称:
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()