Нажмите: как применить действие ко всем командам и подкомандам, но разрешить команде отказаться? - PullRequest
0 голосов
/ 11 мая 2018

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

Я надеялся на что-то вроде (спасибо Стивену Рауху за первоначальную идею):

def do_upgrade():
    print "Performing upgrade"

def bypass_upgrade_check(func):
    setattr(func, "do_upgrade_check", False)
    return func

@click.group()
@click.pass_context
def common(ctx):
    sub_cmd = ctx.command.commands[ctx.invoked_subcommand]
    if getattr(sub_cmd, "do_upgrade_check", True):
        do_upgrade()

@bypass_upgrade_check
@common.command()
def top_cmd1():
    # don't run do_upgrade() on top level command
    pass

@common.command()
def top_cmd2():
    # DO run do_upgrade() on top level command
    pass

@common.group()
def sub_cmd_group():
    pass

@bypass_upgrade_check
@sub_cmd.command()
def sub_cmd1():
    # don't run do_upgrade() on second-level command
    pass

@sub.command()
def sub_cmd2():
    # DO run do_upgrade() on second-level command
    pass

К сожалению, это работает только для команд верхнего уровня, поскольку ctx.invoked_subcommand относится к sub_cmd_group, а не sub_cmd1 или sub_cmd2.

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

1 Ответ

0 голосов
/ 11 мая 2018

Один из способов решить эту проблему - создать собственный декоратор, который соединяется с пользовательским классом click.Group:

Конструктор пользовательских декораторов:

def make_exclude_hook_group(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 invoke(self, ctx):
            """ group invoke which hooks context invoke """
            invoke = ctx.invoke

            def ctx_invoke(*args, **kwargs):
                """ monkey patched context invoke """
                sub_cmd = ctx.command.commands[ctx.invoked_subcommand]
                if not isinstance(sub_cmd, click.Group) and \
                        getattr(sub_cmd, hook_attr_name, True):
                    # invoke the callback
                    callback()
                return invoke(*args, **kwargs)

            ctx.invoke = ctx_invoke

            return super(HookGroup, self).invoke(ctx)

        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 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_check = make_exclude_hook_group(do_upgrade)

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

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

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

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

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

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

В этом случае мы создаем декоратор, который устанавливает атрибут для любой функции щелчка, которая не требует обратного вызова. Затем в нашей пользовательской группе мы исправляем патч click.Context.invoke() нашего контекста, и если команда, которая должна быть выполнена, не оформлена, мы вызываем обратный вызов.

Тестовый код:

import click

def do_upgrade():
    print("Performing upgrade")

bypass_upgrade_check = make_exclude_hook_group(do_upgrade)

@click.group(cls=bypass_upgrade_check())
@click.pass_context
def cli(ctx):
    pass

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

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)]
-----------
> top_cmd1
cmd1
-----------
> top_cmd2
Performing upgrade
cmd2
-----------
> sub_cmd_group sub_cmd1
sub_cmd_group
sub_cmd1
-----------
> sub_cmd_group sub_cmd2
Performing upgrade
sub_cmd_group
sub_cmd2
-----------
> --help
Usage: test.py [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  sub_cmd_group
  top_cmd1
  top_cmd2
...