单击:如何将操作应用于所有命令和子命令但允许命令选择退出(部分 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