单击:在带有上下文对象的链式命令中使用另一个函数
Click: Use another function in chained commands with context object
我最近一直在使用 click package 构建命令行界面,到目前为止效果很好。
现在我在结合上下文对象使用链式命令时遇到了一些麻烦。问题是,当我想从另一个命令中调用另一个命令的函数时,不知何故出错。
这可能与点击中装饰器的使用有某种关系,但我现在没有看到错误。
这是我的代码的一个最小示例:
import click
@click.group(chain=True)
@click.option('--some_common_option', type=float, default=1e-10)
@click.pass_context
def cli(ctx, some_common_option):
# save shared params within context object for different commands
for k, v in locals().items():
if 'ctx' not in k:
ctx.obj[k] = v
return True
@cli.command()
@click.argument('some_argument', type=str)
@click.pass_context
def say_something(ctx, some_argument):
print(some_argument)
return True
@cli.command()
@click.argument('some_other_argument', type=str)
@click.pass_context
def say_more(ctx, some_other_argument):
ctx.obj['text'] = some_other_argument
say_something(ctx, ctx.obj['text'])
return True
if __name__ == '__main__':
cli(obj={})
这是终端上提供的错误:
$ python test.py say_something 'Hello!'
Hello!
$ python test.py say_more 'How are you?'
Traceback (most recent call last):
File "test.py", line 36, in <module>
cli(obj={})
File "/home/user/.anaconda3/lib/python3.6/site-packages/click/core.py", line 722, in __call__
return self.main(*args, **kwargs)
File "/home/user/.anaconda3/lib/python3.6/site-packages/click/core.py", line 697, in main
rv = self.invoke(ctx)
File "/home/user/.anaconda3/lib/python3.6/site-packages/click/core.py", line 1092, in invoke
rv.append(sub_ctx.command.invoke(sub_ctx))
File "/home/user/.anaconda3/lib/python3.6/site-packages/click/core.py", line 895, in invoke
return ctx.invoke(self.callback, **ctx.params)
File "/home/user/.anaconda3/lib/python3.6/site-packages/click/core.py", line 535, in invoke
return callback(*args, **kwargs)
File "/home/user/.anaconda3/lib/python3.6/site-packages/click/decorators.py", line 17, in new_func
return f(get_current_context(), *args, **kwargs)
File "test.py", line 30, in say_more
say_something(ctx, ctx.obj['text'])
File "/home/user/.anaconda3/lib/python3.6/site-packages/click/core.py", line 722, in __call__
return self.main(*args, **kwargs)
File "/home/user/.anaconda3/lib/python3.6/site-packages/click/core.py", line 683, in main
args = list(args)
TypeError: 'Context' object is not iterable
$
我想知道为什么以及在何处对上下文对象进行迭代。
有什么提示可以解决这个问题并在另一个命令中使用该功能吗?
如果您可以编辑您的点击命令功能,您可以像这样组织它们:
@cli.command()
@click.argument('some_argument', type=str)
@click.pass_context
def say_something(ctx, some_argument):
return _say_something(ctx, some_argument):
def _say_something(ctx, some_argument):
print(some_argument)
如果像这样构建,那么您可以调用 _say_something()
函数作为未修饰的(正常)Python 函数。
如果您无法编辑命令
在 this answer 的基础上,您可以使用此功能将上下文传递给另一个点击命令:
代码:
def call_click_command_with_ctx(cmd, ctx, *args, **kwargs):
""" Wrapper to call a click command with a Context object
:param cmd: click cli command function to call
:param ctx: click context
:param args: arguments to pass to the function
:param kwargs: keyword arguments to pass to the function
:return: None
"""
# monkey patch make_context
def make_context(*some_args, **some_kwargs):
child_ctx = click.Context(cmd, parent=ctx)
with child_ctx.scope(cleanup=False):
cmd.parse_args(child_ctx, list(args))
return child_ctx
cmd.make_context = make_context
prev_make_context = cmd.make_context
# call the command
call_click_command(cmd, *args, **kwargs)
# restore make_context
cmd.make_context = prev_make_context
这是如何工作的?
之所以可行,是因为 click 是一个设计良好的 OO 框架。可以内省 @click.Command
对象以确定它期望的参数。然后可以构造一个命令行,它看起来像 click 所期望的命令行。此外,可以覆盖命令的 make_context
方法以允许命令使用命令上下文。
来自 Previous Answer 的代码:
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)
测试代码:
import click
@click.group(chain=True)
@click.option('--some_common_option', type=float, default=1e-10)
@click.pass_context
def cli(ctx, some_common_option):
# save shared params within context object for different commands
for k, v in locals().items():
if 'ctx' not in k:
ctx.obj[k] = v
@cli.command()
@click.argument('some_argument', type=str)
@click.pass_context
def say_something(ctx, some_argument):
print(some_argument)
@cli.command()
@click.argument('some_other_argument', type=str)
@click.pass_context
def say_more(ctx, some_other_argument):
ctx.obj['text'] = some_other_argument
call_click_command_with_ctx(say_something, ctx, ctx.obj['text'])
if __name__ == "__main__":
commands = (
'say_something something',
'say_more more',
'--help',
)
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('> ' + cmd)
time.sleep(0.1)
cli(cmd.split(), obj={})
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)]
-----------
> say_something something
something
-----------
> say_more more
more
-----------
> --help
Usage: test.py [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...
Options:
--some_common_option FLOAT
--help Show this message and exit.
Commands:
say_more
say_something
我最近一直在使用 click package 构建命令行界面,到目前为止效果很好。
现在我在结合上下文对象使用链式命令时遇到了一些麻烦。问题是,当我想从另一个命令中调用另一个命令的函数时,不知何故出错。
这可能与点击中装饰器的使用有某种关系,但我现在没有看到错误。
这是我的代码的一个最小示例:
import click
@click.group(chain=True)
@click.option('--some_common_option', type=float, default=1e-10)
@click.pass_context
def cli(ctx, some_common_option):
# save shared params within context object for different commands
for k, v in locals().items():
if 'ctx' not in k:
ctx.obj[k] = v
return True
@cli.command()
@click.argument('some_argument', type=str)
@click.pass_context
def say_something(ctx, some_argument):
print(some_argument)
return True
@cli.command()
@click.argument('some_other_argument', type=str)
@click.pass_context
def say_more(ctx, some_other_argument):
ctx.obj['text'] = some_other_argument
say_something(ctx, ctx.obj['text'])
return True
if __name__ == '__main__':
cli(obj={})
这是终端上提供的错误:
$ python test.py say_something 'Hello!'
Hello!
$ python test.py say_more 'How are you?'
Traceback (most recent call last):
File "test.py", line 36, in <module>
cli(obj={})
File "/home/user/.anaconda3/lib/python3.6/site-packages/click/core.py", line 722, in __call__
return self.main(*args, **kwargs)
File "/home/user/.anaconda3/lib/python3.6/site-packages/click/core.py", line 697, in main
rv = self.invoke(ctx)
File "/home/user/.anaconda3/lib/python3.6/site-packages/click/core.py", line 1092, in invoke
rv.append(sub_ctx.command.invoke(sub_ctx))
File "/home/user/.anaconda3/lib/python3.6/site-packages/click/core.py", line 895, in invoke
return ctx.invoke(self.callback, **ctx.params)
File "/home/user/.anaconda3/lib/python3.6/site-packages/click/core.py", line 535, in invoke
return callback(*args, **kwargs)
File "/home/user/.anaconda3/lib/python3.6/site-packages/click/decorators.py", line 17, in new_func
return f(get_current_context(), *args, **kwargs)
File "test.py", line 30, in say_more
say_something(ctx, ctx.obj['text'])
File "/home/user/.anaconda3/lib/python3.6/site-packages/click/core.py", line 722, in __call__
return self.main(*args, **kwargs)
File "/home/user/.anaconda3/lib/python3.6/site-packages/click/core.py", line 683, in main
args = list(args)
TypeError: 'Context' object is not iterable
$
我想知道为什么以及在何处对上下文对象进行迭代。
有什么提示可以解决这个问题并在另一个命令中使用该功能吗?
如果您可以编辑您的点击命令功能,您可以像这样组织它们:
@cli.command()
@click.argument('some_argument', type=str)
@click.pass_context
def say_something(ctx, some_argument):
return _say_something(ctx, some_argument):
def _say_something(ctx, some_argument):
print(some_argument)
如果像这样构建,那么您可以调用 _say_something()
函数作为未修饰的(正常)Python 函数。
如果您无法编辑命令
在 this answer 的基础上,您可以使用此功能将上下文传递给另一个点击命令:
代码:
def call_click_command_with_ctx(cmd, ctx, *args, **kwargs):
""" Wrapper to call a click command with a Context object
:param cmd: click cli command function to call
:param ctx: click context
:param args: arguments to pass to the function
:param kwargs: keyword arguments to pass to the function
:return: None
"""
# monkey patch make_context
def make_context(*some_args, **some_kwargs):
child_ctx = click.Context(cmd, parent=ctx)
with child_ctx.scope(cleanup=False):
cmd.parse_args(child_ctx, list(args))
return child_ctx
cmd.make_context = make_context
prev_make_context = cmd.make_context
# call the command
call_click_command(cmd, *args, **kwargs)
# restore make_context
cmd.make_context = prev_make_context
这是如何工作的?
之所以可行,是因为 click 是一个设计良好的 OO 框架。可以内省 @click.Command
对象以确定它期望的参数。然后可以构造一个命令行,它看起来像 click 所期望的命令行。此外,可以覆盖命令的 make_context
方法以允许命令使用命令上下文。
来自 Previous Answer 的代码:
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)
测试代码:
import click
@click.group(chain=True)
@click.option('--some_common_option', type=float, default=1e-10)
@click.pass_context
def cli(ctx, some_common_option):
# save shared params within context object for different commands
for k, v in locals().items():
if 'ctx' not in k:
ctx.obj[k] = v
@cli.command()
@click.argument('some_argument', type=str)
@click.pass_context
def say_something(ctx, some_argument):
print(some_argument)
@cli.command()
@click.argument('some_other_argument', type=str)
@click.pass_context
def say_more(ctx, some_other_argument):
ctx.obj['text'] = some_other_argument
call_click_command_with_ctx(say_something, ctx, ctx.obj['text'])
if __name__ == "__main__":
commands = (
'say_something something',
'say_more more',
'--help',
)
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('> ' + cmd)
time.sleep(0.1)
cli(cmd.split(), obj={})
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)]
-----------
> say_something something
something
-----------
> say_more more
more
-----------
> --help
Usage: test.py [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...
Options:
--some_common_option FLOAT
--help Show this message and exit.
Commands:
say_more
say_something