我有一个使用 click 包装为命令的函数。所以它看起来像这样:
@click.command()
@click.option('-w', '--width', type=int, help="Some helping message", default=0)
[... some other options ...]
def app(width, [... some other option arguments...]):
[... function code...]
我对此功能有不同的用例。有时候通过命令行调用也可以,但有时候我也想直接调用函数
from file_name import app
width = 45
app(45, [... other arguments ...])
我们怎样才能做到这一点?我们如何使用 click 来调用已包装为命令的函数?我找到了这篇相关帖子,但我不清楚如何使其适应我的情况(即从头开始构建一个 Context 类并在单击命令功能之外使用它)。
编辑:我应该提到:我无法(轻松)修改包含要调用的函数的包。所以我正在寻找的解决方案是如何从调用方处理它。
我尝试使用 Python 3.7 并单击 7 以下代码:
import click
@click.command()
@click.option('-w', '--width', type=int, default=0)
@click.option('--option2')
@click.argument('argument')
def app(width, option2, argument):
click.echo("params: {} {} {}".format(width, option2, argument))
assert width == 3
assert option2 == '4'
assert argument == 'arg'
app(["arg", "--option2", "4", "-w", 3], standalone_mode=False)
app(["arg", "-w", 3, "--option2", "4" ], standalone_mode=False)
app(["-w", 3, "--option2", "4", "arg"], standalone_mode=False)
所有
app
通话均正常!
您可以通过从参数重建命令行来从常规代码调用
click
命令函数。使用您的示例,它可能看起来像这样:
call_click_command(app, width, [... other arguments ...])
def call_click_command(cmd, *args, **kwargs):
""" Wrapper to call a click command
:param cmd: click cli command function to call
:param args: arguments to pass to the function
:param kwargs: keywrod arguments to pass to the function
:return: None
"""
# Get positional arguments from args
arg_values = {c.name: a for a, c in zip(args, cmd.params)}
args_needed = {c.name: c for c in cmd.params
if c.name not in arg_values}
# build and check opts list from kwargs
opts = {a.name: a for a in cmd.params if isinstance(a, click.Option)}
for name in kwargs:
if name in opts:
arg_values[name] = kwargs[name]
else:
if name in args_needed:
arg_values[name] = kwargs[name]
del args_needed[name]
else:
raise click.BadParameter(
"Unknown keyword argument '{}'".format(name))
# check positional arguments list
for arg in (a for a in cmd.params if isinstance(a, click.Argument)):
if arg.name not in arg_values:
raise click.BadParameter("Missing required positional"
"parameter '{}'".format(arg.name))
# build parameter lists
opts_list = sum(
[[o.opts[0], str(arg_values[n])] for n, o in opts.items()], [])
args_list = [str(v) for n, v in arg_values.items() if n not in opts]
# call the command
cmd(opts_list + args_list)
这是可行的,因为 click 是一个设计良好的 OO 框架。可以自省
@click.Command
对象以确定它期望的参数。然后可以构建一个命令行,它看起来像单击所期望的命令行。
import click
@click.command()
@click.option('-w', '--width', type=int, default=0)
@click.option('--option2')
@click.argument('argument')
def app(width, option2, argument):
click.echo("params: {} {} {}".format(width, option2, argument))
assert width == 3
assert option2 == '4'
assert argument == 'arg'
width = 3
option2 = 4
argument = 'arg'
if __name__ == "__main__":
commands = (
(width, option2, argument, {}),
(width, option2, dict(argument=argument)),
(width, dict(option2=option2, argument=argument)),
(dict(width=width, option2=option2, argument=argument),),
)
import sys, time
time.sleep(1)
print('Click Version: {}'.format(click.__version__))
print('Python Version: {}'.format(sys.version))
for cmd in commands:
try:
time.sleep(0.1)
print('-----------')
print('> {}'.format(cmd))
time.sleep(0.1)
call_click_command(app, *cmd[:-1], **cmd[-1])
except BaseException as exc:
if str(exc) != '0' and \
not isinstance(exc, (click.ClickException, SystemExit)):
raise
Click Version: 6.7
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct 3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> (3, 4, 'arg', {})
params: 3 4 arg
-----------
> (3, 4, {'argument': 'arg'})
params: 3 4 arg
-----------
> (3, {'option2': 4, 'argument': 'arg'})
params: 3 4 arg
-----------
> ({'width': 3, 'option2': 4, 'argument': 'arg'},)
params: 3 4 arg
如果只想调用底层函数,可以直接访问为
click.Command.callback
。 Click 将底层包装的 Python 函数存储为类成员。请注意,直接调用该函数将绕过所有单击验证,并且不会出现任何单击上下文信息。
这里是一个示例代码,它迭代当前 Python 模块中的所有
click.Command
对象,并从中创建可调用函数的字典。
from functools import partial
from inspect import getmembers
import click
all_functions_of_click_commands = {}
def _call_click_command(cmd: click.Command, *args, **kwargs):
result = cmd.callback(*args, **kwargs)
return result
# Pull out all Click commands from the current module
module = sys.modules[__name__]
for name, obj in getmembers(module):
if isinstance(obj, click.Command) and not isinstance(obj, click.Group):
# Create a wrapper Python function that calls click Command.
# Click uses dash in command names and dash is not valid Python syntax
name = name.replace("-", "_")
# We also set docstring of this function correctly.
func = partial(_call_click_command, obj)
func.__doc__ = obj.__doc__
all_functions_of_click_commands[name] = func
完整示例可以在 binance-api-test-tool 源代码中找到。
此用例在文档中进行了描述。
有时,从一个命令调用另一个命令可能会很有趣。 Click 通常不鼓励这种模式,但尽管如此,这种模式还是有可能的。为此,您可以使用 Context.invoke() 或 Context.forward() 方法。
cli = click.Group()
@cli.command()
@click.option('--count', default=1)
def test(count):
click.echo('Count: %d' % count)
@cli.command()
@click.option('--count', default=1)
@click.pass_context
def dist(ctx, count):
ctx.forward(test)
ctx.invoke(test, count=42)
它们的工作原理类似,但不同之处在于 Context.invoke() 仅使用您作为调用者提供的参数调用另一个命令,而 Context.forward() 则填充当前命令中的参数。
这是使用选项字典调用点击函数的解决方案:
有时,从另一个命令调用一个命令可能会很有趣 命令。 Click 通常不鼓励这种模式, 但仍然有可能。为此,您可以使用 Context.invoke() 或 Context.forward() 方法。
它们的工作原理类似,但不同之处在于 Context.invoke() 仅使用您提供的参数调用另一个命令 调用者,而 Context.forward() 填充来自调用者的参数 当前命令。两者都接受命令作为第一个参数并且 其他一切都按照您的预期进行。
示例:
cli = click.Group()
@cli.command()
@click.option('--opt1', default=1)
@click.option('--opt2', default=2)
def test(opt1, opt2):
print(opt1)
print(opt2)
@cli.command()
@click.pass_context
def dist(ctx):
args = {"opt1":3, "opt2": 4}
ctx.invoke(test, **args)
if __name__ == "__main__":
dist()