单击:如何将操作应用于所有命令和子命令但允许命令选择退出(部分 duex)?
Click: how do I apply an action to all commands and subcommands but allow a command to opt-out (part duex)?
在 的基础上,我希望能够在 运行 我的回调之前拥有父组 运行 的主体。
I have a case where I'd like to automatically run a common function, check_upgrade(), for most of my click commands and sub-commands, but there are a few cases where I don't want to run it. I was thinking I could have a decorator that one can add (e.g. @bypass_upgrade_check) for commands where check_upgrade() should not run.
例如:
def do_upgrade():
print("Performing upgrade")
bypass_upgrade_check = make_exclude_hook_group(do_upgrade)
@click.group(cls=bypass_upgrade_check())
@click.option('--arg1', default=DFLT_ARG1)
@click.option('--arg2', default=DFLT_ARG2)
@click.pass_context
def cli(ctx, arg1, arg2):
config.call_me_before_upgrade_check(arg1, arg2)
@bypass_upgrade_check
@cli.command()
def top_cmd1():
click.echo('cmd1')
@cli.command()
def top_cmd2():
click.echo('cmd2')
@cli.group()
def sub_cmd_group():
click.echo('sub_cmd_group')
@bypass_upgrade_check
@sub_cmd_group.command()
def sub_cmd1():
click.echo('sub_cmd1')
@sub_cmd_group.command()
def sub_cmd2():
click.echo('sub_cmd2')
我希望事情像 中解释的那样运行,但我希望它调用:[而不是在执行 cli()
的主体之前执行 do_upgrade()
=22=]
cli() --> do_upgrade() --> top_cmd1()
例如。或者对于嵌套命令:
cli() --> sub_cmd_group() --> do_upgrade() --> sub_cmd1()
所以我想用另一种方式来表达这个问题是:是否有可能从原始问题中获得功能,但是在子命令本身 运行s 之前调用回调而不是在之前调用任何组块 运行?
我需要这个的原因是因为传递给顶级 CLI 命令的参数指示用于检查升级的服务器地址。我需要此信息来处理 do_upgrade()
。我无法将此信息直接传递给 do_upgrade()
,因为此服务器信息也在应用程序的其他地方使用。我可以使用 config.get_server()
.
从 do_upgrade()
查询它
以与 类似的方式,解决此问题的一种方法是构建与自定义 click.Group
class 配对的自定义装饰器。增加的复杂性是挂钩 Command.invoke()
而不是 Group.invoke()
以便回调将在 Command.invoke()
之前立即调用,因此将在任何 Group.invoke()
之后调用:
自定义装饰器生成器:
import click
def make_exclude_hook_command(callback):
""" for any command that is not decorated, call the callback """
hook_attr_name = 'hook_' + callback.__name__
class HookGroup(click.Group):
""" group to hook context invoke to see if the callback is needed"""
def group(self, *args, **kwargs):
""" new group decorator to make sure sub groups are also hooked """
if 'cls' not in kwargs:
kwargs['cls'] = type(self)
return super(HookGroup, self).group(*args, **kwargs)
def command(self, *args, **kwargs):
""" new command decorator to monkey patch command invoke """
cmd = super(HookGroup, self).command(*args, **kwargs)
def hook_command_decorate(f):
# decorate the command
ret = cmd(f)
# grab the original command invoke
orig_invoke = ret.invoke
def invoke(ctx):
"""call the call back right before command invoke"""
parent = ctx.parent
sub_cmd = parent and parent.command.commands[
parent.invoked_subcommand]
if not sub_cmd or \
not isinstance(sub_cmd, click.Group) and \
getattr(sub_cmd, hook_attr_name, True):
# invoke the callback
callback()
return orig_invoke(ctx)
# hook our command invoke to command and return cmd
ret.invoke = invoke
return ret
# return hooked command decorator
return hook_command_decorate
def decorator(func=None):
if func is None:
# if called other than as decorator, return group class
return HookGroup
setattr(func, hook_attr_name, False)
return decorator
使用装饰器生成器:
要使用装饰器,我们首先需要像这样构建装饰器:
bypass_upgrade = make_exclude_hook_command(do_upgrade)
然后我们需要将其作为自定义 class 到 click.group()
来使用,例如:
@click.group(cls=bypass_upgrade())
...
最后,我们可以将任何命令或子命令修饰到不需要使用回调的组,例如:
@bypass_upgrade
@my_group.command()
def my_click_command_without_upgrade():
...
这是如何工作的?
之所以可行,是因为 click 是一个设计良好的 OO 框架。 @click.group()
装饰器通常实例化一个 click.Group
对象,但允许使用 cls
参数覆盖此行为。因此,在我们自己的 class 中继承 click.Group
并覆盖所需的方法是一件相对容易的事情。
在这种情况下,我们构建了一个装饰器,它在任何不需要调用回调的点击函数上设置一个属性。然后在我们的自定义组中,我们覆盖了 group()
和 command()
装饰器,这样我们就可以在命令上猴子修补 invoke()
并且如果即将执行的命令没有装饰完毕,调用回调
测试代码:
def do_upgrade():
click.echo("Performing upgrade")
bypass_upgrade = make_exclude_hook_command(do_upgrade)
@click.group(cls=bypass_upgrade())
@click.pass_context
def cli(ctx):
click.echo('cli')
@bypass_upgrade
@cli.command()
def top_cmd1():
click.echo('cmd1')
@cli.command()
def top_cmd2():
click.echo('cmd2')
@cli.group()
def sub_cmd_group():
click.echo('sub_cmd_group')
@bypass_upgrade
@sub_cmd_group.command()
def sub_cmd1():
click.echo('sub_cmd1')
@sub_cmd_group.command()
def sub_cmd2():
click.echo('sub_cmd2')
if __name__ == "__main__":
commands = (
'top_cmd1',
'top_cmd2',
'sub_cmd_group sub_cmd1',
'sub_cmd_group sub_cmd2',
'--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())
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)]
-----------
> sub_cmd_group sub_cmd2
cli
sub_cmd_group
Performing upgrade
sub_cmd2
-----------
> top_cmd1
cli
cmd1
-----------
> top_cmd2
cli
Performing upgrade
cmd2
-----------
> sub_cmd_group sub_cmd1
cli
sub_cmd_group
sub_cmd1
-----------
> sub_cmd_group sub_cmd2
cli
sub_cmd_group
Performing upgrade
sub_cmd2
-----------
> --help
Usage: test.py [OPTIONS] COMMAND [ARGS]...
Options:
--arg1 TEXT
--arg2 TEXT
--help Show this message and exit.
Commands:
sub_cmd_group
top_cmd1
top_cmd2
在
I have a case where I'd like to automatically run a common function, check_upgrade(), for most of my click commands and sub-commands, but there are a few cases where I don't want to run it. I was thinking I could have a decorator that one can add (e.g. @bypass_upgrade_check) for commands where check_upgrade() should not run.
例如:
def do_upgrade():
print("Performing upgrade")
bypass_upgrade_check = make_exclude_hook_group(do_upgrade)
@click.group(cls=bypass_upgrade_check())
@click.option('--arg1', default=DFLT_ARG1)
@click.option('--arg2', default=DFLT_ARG2)
@click.pass_context
def cli(ctx, arg1, arg2):
config.call_me_before_upgrade_check(arg1, arg2)
@bypass_upgrade_check
@cli.command()
def top_cmd1():
click.echo('cmd1')
@cli.command()
def top_cmd2():
click.echo('cmd2')
@cli.group()
def sub_cmd_group():
click.echo('sub_cmd_group')
@bypass_upgrade_check
@sub_cmd_group.command()
def sub_cmd1():
click.echo('sub_cmd1')
@sub_cmd_group.command()
def sub_cmd2():
click.echo('sub_cmd2')
我希望事情像 例如。或者对于嵌套命令: 所以我想用另一种方式来表达这个问题是:是否有可能从原始问题中获得功能,但是在子命令本身 运行s 之前调用回调而不是在之前调用任何组块 运行? 我需要这个的原因是因为传递给顶级 CLI 命令的参数指示用于检查升级的服务器地址。我需要此信息来处理 cli()
的主体之前执行 do_upgrade()
=22=]
cli() --> do_upgrade() --> top_cmd1()
cli() --> sub_cmd_group() --> do_upgrade() --> sub_cmd1()
do_upgrade()
。我无法将此信息直接传递给 do_upgrade()
,因为此服务器信息也在应用程序的其他地方使用。我可以使用 config.get_server()
.do_upgrade()
查询它
以与 click.Group
class 配对的自定义装饰器。增加的复杂性是挂钩 Command.invoke()
而不是 Group.invoke()
以便回调将在 Command.invoke()
之前立即调用,因此将在任何 Group.invoke()
之后调用:
自定义装饰器生成器:
import click
def make_exclude_hook_command(callback):
""" for any command that is not decorated, call the callback """
hook_attr_name = 'hook_' + callback.__name__
class HookGroup(click.Group):
""" group to hook context invoke to see if the callback is needed"""
def group(self, *args, **kwargs):
""" new group decorator to make sure sub groups are also hooked """
if 'cls' not in kwargs:
kwargs['cls'] = type(self)
return super(HookGroup, self).group(*args, **kwargs)
def command(self, *args, **kwargs):
""" new command decorator to monkey patch command invoke """
cmd = super(HookGroup, self).command(*args, **kwargs)
def hook_command_decorate(f):
# decorate the command
ret = cmd(f)
# grab the original command invoke
orig_invoke = ret.invoke
def invoke(ctx):
"""call the call back right before command invoke"""
parent = ctx.parent
sub_cmd = parent and parent.command.commands[
parent.invoked_subcommand]
if not sub_cmd or \
not isinstance(sub_cmd, click.Group) and \
getattr(sub_cmd, hook_attr_name, True):
# invoke the callback
callback()
return orig_invoke(ctx)
# hook our command invoke to command and return cmd
ret.invoke = invoke
return ret
# return hooked command decorator
return hook_command_decorate
def decorator(func=None):
if func is None:
# if called other than as decorator, return group class
return HookGroup
setattr(func, hook_attr_name, False)
return decorator
使用装饰器生成器:
要使用装饰器,我们首先需要像这样构建装饰器:
bypass_upgrade = make_exclude_hook_command(do_upgrade)
然后我们需要将其作为自定义 class 到 click.group()
来使用,例如:
@click.group(cls=bypass_upgrade())
...
最后,我们可以将任何命令或子命令修饰到不需要使用回调的组,例如:
@bypass_upgrade
@my_group.command()
def my_click_command_without_upgrade():
...
这是如何工作的?
之所以可行,是因为 click 是一个设计良好的 OO 框架。 @click.group()
装饰器通常实例化一个 click.Group
对象,但允许使用 cls
参数覆盖此行为。因此,在我们自己的 class 中继承 click.Group
并覆盖所需的方法是一件相对容易的事情。
在这种情况下,我们构建了一个装饰器,它在任何不需要调用回调的点击函数上设置一个属性。然后在我们的自定义组中,我们覆盖了 group()
和 command()
装饰器,这样我们就可以在命令上猴子修补 invoke()
并且如果即将执行的命令没有装饰完毕,调用回调
测试代码:
def do_upgrade():
click.echo("Performing upgrade")
bypass_upgrade = make_exclude_hook_command(do_upgrade)
@click.group(cls=bypass_upgrade())
@click.pass_context
def cli(ctx):
click.echo('cli')
@bypass_upgrade
@cli.command()
def top_cmd1():
click.echo('cmd1')
@cli.command()
def top_cmd2():
click.echo('cmd2')
@cli.group()
def sub_cmd_group():
click.echo('sub_cmd_group')
@bypass_upgrade
@sub_cmd_group.command()
def sub_cmd1():
click.echo('sub_cmd1')
@sub_cmd_group.command()
def sub_cmd2():
click.echo('sub_cmd2')
if __name__ == "__main__":
commands = (
'top_cmd1',
'top_cmd2',
'sub_cmd_group sub_cmd1',
'sub_cmd_group sub_cmd2',
'--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())
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)]
-----------
> sub_cmd_group sub_cmd2
cli
sub_cmd_group
Performing upgrade
sub_cmd2
-----------
> top_cmd1
cli
cmd1
-----------
> top_cmd2
cli
Performing upgrade
cmd2
-----------
> sub_cmd_group sub_cmd1
cli
sub_cmd_group
sub_cmd1
-----------
> sub_cmd_group sub_cmd2
cli
sub_cmd_group
Performing upgrade
sub_cmd2
-----------
> --help
Usage: test.py [OPTIONS] COMMAND [ARGS]...
Options:
--arg1 TEXT
--arg2 TEXT
--help Show this message and exit.
Commands:
sub_cmd_group
top_cmd1
top_cmd2