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