Нажмите: как применить действие ко всем командам и подкомандам, но разрешить команде отказаться (часть 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