Python Click - отображать справку для подкоманды, даже если исключение вызвано в групповой команде - PullRequest
0 голосов
/ 09 января 2019

Я пишу сценарий командной строки, mycli, с двумя подкомандами:

  • mkcli init (для инициализации пустого проекта с файлом .configrc)
  • mkcli run (для запуска основной логики скрипта).

Как правило, mycli run не должен работать, если файл .configrc не найден в рабочем каталоге. Однако мои пользователи должны иметь возможность просмотреть справочное сообщение для run:

$ mycli run --help
Usage: mycli run [OPTIONS]

Options:
  --dryrun  Run in read-only mode
  --help    Show this message and exit.

Однако это не работает, если .configrc не существует, поскольку FileNotFoundError вызывается в групповой команде clirun никогда не достигается). Я могу запустить подкоманду init без предварительного поиска файла .configrc с помощью ctx.invoked_subcommand (см. Ниже), но я не вижу способа гарантировать, что подкоманда run всегда будет запускаться, если она вызывается с помощью --help.

Если пользователь запускает mkcli run и файл .configrc не найден, мой сценарий завершается с run "mycli init" first. Но mycli run --help должно работать, даже если нет .configrc. Как я могу это сделать? Или кто-нибудь может предложить лучший способ обработки init?

@click.group()
@click.pass_context
def cli(ctx):

    ctx.obj = {}
    if ctx.invoked_subcommand != "init":
        config = yaml.load(open(".configrc").read())
        ctx.obj.update({key: config[key] for key in config})

@cli.command()
@click.pass_context
def init(ctx):
    print("Initialize project.")

@cli.command()
@click.option("--dryrun", type=bool, is_flag=True, help="Run in read-only mode")
@click.pass_context
def run(ctx, dryrun):
    print("Run main program here.")

1 Ответ

0 голосов
/ 12 января 2019

Я бы предложил изменить порядок запуска кода инициализации. Это можно сделать с помощью ...

Пользовательский класс:

class LoadInitForCommands(click.Group):

    def command(self, *args, **kwargs):

        def decorator(f):
            # call the original decorator
            cmd = click.command(*args, **kwargs)(f)
            self.add_command(cmd)
            orig_invoke = cmd.invoke

            def invoke(ctx):
                # Custom init code is here
                ctx.obj = {}
                if cmd.name != "init":
                    config = yaml.load(open(".configrc").read())
                    ctx.obj.update({key: config[key] for key in config})

                # call the original invoke()
                return orig_invoke(ctx)

            # hook the command's invoke
            cmd.invoke = invoke
            return cmd

        return decorator

Использование пользовательского класса:

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

@click.group(cls=LoadInitForCommands)
def cli():
    """"""

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

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

В этом случае мы подключаем декоратор command(), и в этом блоке мы перезаписываем команду invoke(). Это позволяет читать файл инициализации после того, как флаг --help уже обработан.

Обратите внимание, что этот код предназначен для упрощения использования многих команд, для которых --help будет доступен до чтения init. В примере в вопросе есть только одна команда, которая нуждается в инициализации. Если это всегда так, то этот ответ может быть привлекательным.

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

import click
import yaml

@click.group(cls=LoadInitForCommands)
def cli():
    """"""

@cli.command()
@click.pass_context
def init(ctx):
    print("Initialize project.")


@cli.command()
@click.option("--dryrun", type=bool, is_flag=True,
              help="Run in read-only mode")
@click.pass_context
def run(ctx, dryrun):
    print("Run main program here.")


if __name__ == "__main__":
    commands = (
        'init',
        'run --help',
        'run',
        '--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(), obj={})

        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)]
-----------
> init
Initialize project.
-----------
> run --help
Usage: test.py run [OPTIONS]

Options:
  --dryrun  Run in read-only mode
  --help    Show this message and exit.
-----------
> run
Traceback (most recent call last):
  File "C:\Users\stephen\AppData\Local\JetBrains\PyCharm 2018.3\helpers\pydev\pydevd.py", line 1741, in <module>
    main()
  File "C:\Users\stephen\AppData\Local\JetBrains\PyCharm 2018.3\helpers\pydev\pydevd.py", line 1735, in main
    globals = debugger.run(setup['file'], None, None, is_module)
  File "C:\Users\stephen\AppData\Local\JetBrains\PyCharm 2018.3\helpers\pydev\pydevd.py", line 1135, in run
    pydev_imports.execfile(file, globals, locals)  # execute the script
  File "C:\Users\stephen\AppData\Local\JetBrains\PyCharm 2018.3\helpers\pydev\_pydev_imps\_pydev_execfile.py", line 18, in execfile
    exec(compile(contents+"\n", file, 'exec'), glob, loc)
  File "C:/Users/stephen/Documents/src/testcode/test.py", line 77, in <module>
    cli(cmd.split(), obj={})
  File "C:\Users\stephen\AppData\Local\Programs\Python\Python36\lib\site-packages\click\core.py", line 722, in __call__
    return self.main(*args, **kwargs)
  File "C:\Users\stephen\AppData\Local\Programs\Python\Python36\lib\site-packages\click\core.py", line 697, in main
    rv = self.invoke(ctx)
  File "C:\Users\stephen\AppData\Local\Programs\Python\Python36\lib\site-packages\click\core.py", line 1066, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "C:/Users/stephen/Documents/src/testcode/test.py", line 26, in invoke
    config = yaml.load(open(".configrc").read())
FileNotFoundError: [Errno 2] No such file or directory: '.configrc'
...