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

1

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

У меня есть случай, когда я хочу автоматически запускать общую функцию check_upgrade() для большинства моих команд и подкоманд, но есть несколько случаев, когда я не хочу ее запускать. Я думал, что у меня может быть декоратор, который можно добавить (например, @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().

Теги:
python-click

1 ответ

1
Лучший ответ

Аналогично click.Group вопросу, одним из способов решения этой проблемы является создание пользовательского декоратора, который click.Group с пользовательским классом click.Group. Дополнительное осложнение состоит в том, чтобы перехватить 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)

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

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

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

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

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

Это работает, потому что клик - это хорошо разработанная OO-структура. @click.group() обычно создает экземпляр объекта click.Group но позволяет этому поведению click.Group параметр cls. Таким образом, относительно легко наследовать от click.Group в нашем классе и над click.Group желаемые методы.

В этом случае мы создаем декоратор, который устанавливает атрибут для любой функции щелчка, которая не требует вызова обратного вызова. Затем в нашей настраиваемой группе мы переопределяем как декораторы group() и command() чтобы мы могли использовать команду monkey patch 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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню