Нажмите: как применить действие ко всем командам и подкомандам, но разрешить команде отказаться (часть duex)?

Основываясь на моем первоначальном вопросе, я хотел бы иметь возможность запустить тело родительской группы перед выполнением обратного вызова.

У меня есть случай, когда я хотел бы автоматически запускать общую функцию check_upgrade() для большинства моих команд и подкоманд click, но в некоторых случаях я не хочу ее запускать. Я думал, что мог бы иметь декоратор, который можно добавить (например, @bypass_upgrade_check) для команд, где check_upgrade() не должен запускаться.

Например:

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')

Я бы хотел, чтобы все функционировало так, как описано в первоначальном вопросе, но вместо выполнения do_upgrade() перед выполнением тела cli()Я хотел бы это позвонить:

cli() --> do_upgrade() --> top_cmd1()

например. Или для вложенной команды:

cli() --> sub_cmd_group() --> do_upgrade() --> sub_cmd1()

Поэтому я думаю, что другой способ сформулировать вопрос таков: возможно ли получить функциональность из исходного вопроса, но можно ли вызвать обратный вызов непосредственно перед выполнением самой подкоманды вместо вызова до того, как будет запущен какой-либо из блоков группы?

Причина, по которой мне это нужно, заключается в том, что аргументы, передаваемые команде CLI верхнего уровня, указывают адрес сервера для проверки на обновление. Мне нужна эта информация для обработки do_upgrade(), Я не могу передать эту информацию напрямую do_upgrade() потому что эта информация о сервере также используется в другом месте в приложении. Я могу запросить это от do_upgrade() с чем-то вроде config.get_server(),

1 ответ

Решение

По аналогии с первоначальным вопросом, один из способов решить эту проблему - создать собственный декоратор, который соединяется с пользовательским click.Group учебный класс. Дополнительное осложнение заключается в том, чтобы зацепить Command.invoke() вместо Group.invoke() так что обратный вызов будет вызван непосредственно перед Command.invoke() и, таким образом, будет вызываться после любого Group.invoke():

Custom Decorator Builder:

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)

Затем нам нужно использовать его как пользовательский класс для click.group() лайк:

@click.group(cls=bypass_upgrade())
...

И, наконец, мы можем декорировать любые команды или подкоманды для группы, которым не нужно использовать обратный вызов, например:

@bypass_upgrade
@my_group.command()
def my_click_command_without_upgrade():
     ...

Как это работает?

Это работает, потому что щелчок - это хорошо спроектированная структура OO. @click.group() декоратор обычно создает экземпляр click.Group объект, но позволяет переопределить это поведение с cls параметр. Так что это относительно легко унаследовать от 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
Другие вопросы по тегам