您如何根据单个选项在 Python 中的单击 CLI 中定义控制流并将其余部分传递给另一个命令?
How do you define control flow in a click CLI in Python based on a single option and pass the rest to another command?
这个问题是关于 Python click 包的,并且与基于传递给 CLI 的参数的控制流有关。
我正在尝试构建一个主 CLI 以位于我的存储库的顶级目录中,它将从一个中央位置控制许多不同的操作模式。当您调用此命令时,它可能需要 n
个选项,其中第一个选项将确定要调用哪个模块,然后将 n-1
args 传递给另一个模块。但我希望每个模块的命令和选项都在其各自的模块中定义,而不是在主 CLI 控制器中定义,因为我试图保持主控制器简单并且每个模块都很好地抽象出来。
这是我想要的一个简单示例:
import click
@click.command()
@click.option('--foo-arg', default='asdf')
def foo(foo_arg):
click.echo(f"Hello from foo with arg {foo_arg}")
@click.command()
@click.option('--bar-arg', default='zxcv')
def bar(bar_arg):
click.echo(f"Hello from bar with arg {bar_arg}")
@click.command()
@click.option('--mode', type=click.Choice(["foo", "bar"]))
def cli(mode):
if mode == "foo":
foo()
if mode == "bar":
bar()
if __name__ == '__main__':
cli()
在这个例子中,foo()
和 bar()
应该被认为是隐藏在 repo 中的模块,它们可能还需要大量的 CLI 选项,我不希望这样使主 cli.py
超载。出于上下文原因,我觉得 foo()
的 CLI 选项应该位于 foo
中。以下是我希望它如何工作。
示例 1:
python -m cli --mode foo --foo-arg asdf
应该生产
Hello from foo with arg asdf
示例 2:
python -m cli --mode bar --bar-arg zxcv
应该生产
Hello from bar with arg zxcv
示例 3:
python -m cli --mode foo --bar-arg qwer
应该会失败,因为 foo()
没有 --bar-arg
选项。
免责声明:我知道我可以将 foo
和 bar
注册为单独的命令(通过 python -m cli foo --foo-arg asd
调用,即使用 foo
而不是 --foo
).但是,由于超出此问题范围的原因,我需要 foo
或 bar
由 --mode
选项标识符指定。这是与我的应用程序交互的工具的限制,不幸的是,这是我无法控制的。
有没有一种方法可以解析 args 并基于 args 的子集使控制流成为可能,然后将其余的传递给后续模块,同时不将每个模块的选项定义为 def cli()
上的装饰器?
您可以使用回调进行验证:https://click.palletsprojects.com/en/7.x/options/#callbacks-for-validation
此外,使用多链接命令,因此每个参数都将传递给它需要的命令。
https://click.palletsprojects.com/en/7.x/commands/#multi-command-chaining
使用选项调用子命令可以通过使用 click.MultiCommand
和自定义 parse_args()
方法来实现,例如:
自定义Class
def mode_opts_cmds(mode_opt_name, namespace=None):
class RemoteCommandsAsModeOpts(click.MultiCommand):
def __init__(self, *args, **kwargs):
super(RemoteCommandsAsModeOpts, self).__init__(*args, **kwargs)
self.mode_opt_name = '--{}'.format(mode_opt_name)
opt = next(p for p in self.params if p.name == mode_opt_name)
assert isinstance(opt.type, click.Choice)
choices = set(opt.type.choices)
self.commands = {k: v for k, v in (
namespace or globals()).items() if k in choices}
for command in self.commands.values():
assert isinstance(command, click.Command)
def parse_args(self, ctx, args):
try:
args.remove(self.mode_opt_name)
except ValueError:
pass
super(RemoteCommandsAsModeOpts, self).parse_args(ctx, args)
def list_commands(self, ctx):
return sorted(self.commands)
def get_command(self, ctx, name):
return self.commands[name]
return RemoteCommandsAsModeOpts
使用自定义 Class
要使用自定义 class,请调用 mode_opts_cmds()
函数创建自定义 class,然后使用 cls
参数传递 class 到 click.group()
装饰器。
@click.group(cls=mode_opts_cmds('mode'))
@click.option('--mode', type=click.Choice(["foo", "bar"]))
def cli(mode):
"""My wonderful cli"""
这是如何工作的?
之所以可行,是因为 click 是一个设计良好的 OO 框架。 @click.group()
装饰器通常实例化一个 click.Group
对象,但允许使用 cls
参数覆盖此行为。因此,在我们自己的 class 中继承 click.Group
并覆盖所需的方法是一件相对容易的事情。
在这种情况下,我们超越了 click.Group.parse_args()
,这样当解析命令行时,我们只需删除 --mode
,然后命令行就像 normal[=50] 一样解析=] 子命令。
使它成为一个库函数
如果自定义 class 将驻留在库中,则需要传入命名空间而不是使用库文件中的默认值 globals()
。自定义 class creator 方法然后需要用类似的东西调用:
@click.group(cls=mode_opts_cmds('mode', namespace=globals()))
测试代码
import click
@click.command()
@click.option('--foo-arg', default='asdf')
def foo(foo_arg):
"""the foo command is ok"""
click.echo(f"Hello from foo with arg {foo_arg}")
@click.command()
@click.option('--bar-arg', default='zxcv')
def bar(bar_arg):
"""bar is my favorite"""
click.echo(f"Hello from bar with arg {bar_arg}")
@click.group(cls=mode_opts_cmds('mode'))
@click.option('--mode', type=click.Choice(["foo", "bar"]))
def cli(mode):
"""My wonderful cli command"""
if __name__ == "__main__":
commands = (
'--mode foo --foo-arg asdf',
'--mode bar --bar-arg zxcv',
'--mode foo --bar-arg qwer',
'--mode foo --help',
'',
)
import sys, time
time.sleep(1)
print('Click Version: {}'.format(click.__version__))
print('Python Version: {}'.format(sys.version))
for command in commands:
try:
time.sleep(0.1)
print('-----------')
print('> ' + command)
time.sleep(0.1)
cli(command.split())
except BaseException as exc:
if str(exc) != '0' and \
not isinstance(exc,
(click.ClickException, SystemExit)):
raise
测试结果
Click Version: 7.0
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct 3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> --mode foo --foo-arg asdf
Hello from foo with arg asdf
-----------
> --mode bar --bar-arg zxcv
Hello from bar with arg zxcv
-----------
> --mode foo --bar-arg qwer
Usage: test.py foo [OPTIONS]
Try "test.py foo --help" for help.
Error: no such option: --bar-arg
-----------
> --mode foo --help
Usage: test.py foo [OPTIONS]
the foo command is ok
Options:
--foo-arg TEXT
--help Show this message and exit.
-----------
>
Usage: test.py [OPTIONS] COMMAND [ARGS]...
My wonderful cli command
Options:
--mode [foo|bar]
--help Show this message and exit.
Commands:
bar bar is my favorite
foo the foo command is ok
这个问题是关于 Python click 包的,并且与基于传递给 CLI 的参数的控制流有关。
我正在尝试构建一个主 CLI 以位于我的存储库的顶级目录中,它将从一个中央位置控制许多不同的操作模式。当您调用此命令时,它可能需要 n
个选项,其中第一个选项将确定要调用哪个模块,然后将 n-1
args 传递给另一个模块。但我希望每个模块的命令和选项都在其各自的模块中定义,而不是在主 CLI 控制器中定义,因为我试图保持主控制器简单并且每个模块都很好地抽象出来。
这是我想要的一个简单示例:
import click
@click.command()
@click.option('--foo-arg', default='asdf')
def foo(foo_arg):
click.echo(f"Hello from foo with arg {foo_arg}")
@click.command()
@click.option('--bar-arg', default='zxcv')
def bar(bar_arg):
click.echo(f"Hello from bar with arg {bar_arg}")
@click.command()
@click.option('--mode', type=click.Choice(["foo", "bar"]))
def cli(mode):
if mode == "foo":
foo()
if mode == "bar":
bar()
if __name__ == '__main__':
cli()
在这个例子中,foo()
和 bar()
应该被认为是隐藏在 repo 中的模块,它们可能还需要大量的 CLI 选项,我不希望这样使主 cli.py
超载。出于上下文原因,我觉得 foo()
的 CLI 选项应该位于 foo
中。以下是我希望它如何工作。
示例 1:
python -m cli --mode foo --foo-arg asdf
应该生产
Hello from foo with arg asdf
示例 2:
python -m cli --mode bar --bar-arg zxcv
应该生产
Hello from bar with arg zxcv
示例 3:
python -m cli --mode foo --bar-arg qwer
应该会失败,因为 foo()
没有 --bar-arg
选项。
免责声明:我知道我可以将 foo
和 bar
注册为单独的命令(通过 python -m cli foo --foo-arg asd
调用,即使用 foo
而不是 --foo
).但是,由于超出此问题范围的原因,我需要 foo
或 bar
由 --mode
选项标识符指定。这是与我的应用程序交互的工具的限制,不幸的是,这是我无法控制的。
有没有一种方法可以解析 args 并基于 args 的子集使控制流成为可能,然后将其余的传递给后续模块,同时不将每个模块的选项定义为 def cli()
上的装饰器?
您可以使用回调进行验证:https://click.palletsprojects.com/en/7.x/options/#callbacks-for-validation
此外,使用多链接命令,因此每个参数都将传递给它需要的命令。
https://click.palletsprojects.com/en/7.x/commands/#multi-command-chaining
使用选项调用子命令可以通过使用 click.MultiCommand
和自定义 parse_args()
方法来实现,例如:
自定义Class
def mode_opts_cmds(mode_opt_name, namespace=None):
class RemoteCommandsAsModeOpts(click.MultiCommand):
def __init__(self, *args, **kwargs):
super(RemoteCommandsAsModeOpts, self).__init__(*args, **kwargs)
self.mode_opt_name = '--{}'.format(mode_opt_name)
opt = next(p for p in self.params if p.name == mode_opt_name)
assert isinstance(opt.type, click.Choice)
choices = set(opt.type.choices)
self.commands = {k: v for k, v in (
namespace or globals()).items() if k in choices}
for command in self.commands.values():
assert isinstance(command, click.Command)
def parse_args(self, ctx, args):
try:
args.remove(self.mode_opt_name)
except ValueError:
pass
super(RemoteCommandsAsModeOpts, self).parse_args(ctx, args)
def list_commands(self, ctx):
return sorted(self.commands)
def get_command(self, ctx, name):
return self.commands[name]
return RemoteCommandsAsModeOpts
使用自定义 Class
要使用自定义 class,请调用 mode_opts_cmds()
函数创建自定义 class,然后使用 cls
参数传递 class 到 click.group()
装饰器。
@click.group(cls=mode_opts_cmds('mode'))
@click.option('--mode', type=click.Choice(["foo", "bar"]))
def cli(mode):
"""My wonderful cli"""
这是如何工作的?
之所以可行,是因为 click 是一个设计良好的 OO 框架。 @click.group()
装饰器通常实例化一个 click.Group
对象,但允许使用 cls
参数覆盖此行为。因此,在我们自己的 class 中继承 click.Group
并覆盖所需的方法是一件相对容易的事情。
在这种情况下,我们超越了 click.Group.parse_args()
,这样当解析命令行时,我们只需删除 --mode
,然后命令行就像 normal[=50] 一样解析=] 子命令。
使它成为一个库函数
如果自定义 class 将驻留在库中,则需要传入命名空间而不是使用库文件中的默认值 globals()
。自定义 class creator 方法然后需要用类似的东西调用:
@click.group(cls=mode_opts_cmds('mode', namespace=globals()))
测试代码
import click
@click.command()
@click.option('--foo-arg', default='asdf')
def foo(foo_arg):
"""the foo command is ok"""
click.echo(f"Hello from foo with arg {foo_arg}")
@click.command()
@click.option('--bar-arg', default='zxcv')
def bar(bar_arg):
"""bar is my favorite"""
click.echo(f"Hello from bar with arg {bar_arg}")
@click.group(cls=mode_opts_cmds('mode'))
@click.option('--mode', type=click.Choice(["foo", "bar"]))
def cli(mode):
"""My wonderful cli command"""
if __name__ == "__main__":
commands = (
'--mode foo --foo-arg asdf',
'--mode bar --bar-arg zxcv',
'--mode foo --bar-arg qwer',
'--mode foo --help',
'',
)
import sys, time
time.sleep(1)
print('Click Version: {}'.format(click.__version__))
print('Python Version: {}'.format(sys.version))
for command in commands:
try:
time.sleep(0.1)
print('-----------')
print('> ' + command)
time.sleep(0.1)
cli(command.split())
except BaseException as exc:
if str(exc) != '0' and \
not isinstance(exc,
(click.ClickException, SystemExit)):
raise
测试结果
Click Version: 7.0
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct 3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> --mode foo --foo-arg asdf
Hello from foo with arg asdf
-----------
> --mode bar --bar-arg zxcv
Hello from bar with arg zxcv
-----------
> --mode foo --bar-arg qwer
Usage: test.py foo [OPTIONS]
Try "test.py foo --help" for help.
Error: no such option: --bar-arg
-----------
> --mode foo --help
Usage: test.py foo [OPTIONS]
the foo command is ok
Options:
--foo-arg TEXT
--help Show this message and exit.
-----------
>
Usage: test.py [OPTIONS] COMMAND [ARGS]...
My wonderful cli command
Options:
--mode [foo|bar]
--help Show this message and exit.
Commands:
bar bar is my favorite
foo the foo command is ok