Как вы определяете поток управления в CLI щелчка в Python на основе одного параметра, а остальные передаете другой команде? - PullRequest
1 голос
/ 03 ноября 2019

Этот вопрос касается пакета Python click и касается потока управления, основанного на аргументах, передаваемых в CLI.

Я пытаюсь создать основной CLI, чтобы он находился на вершинекаталог моего репо, который будет контролировать много разных режимов работы из одного центрального места. Когда вы вызываете эту команду, она может принять n параметров, где первый определит, какой модуль вызывать, а затем передаст аргументы n-1 другому модулю. Но я хочу, чтобы команды и опции каждого модуля были определены в соответствующем модуле, а не в главном контроллере CLI, так как я стараюсь, чтобы основной контроллер был простым, а каждый модуль был хорошо удален.

Вот простой пример того, что я хочу:

import click


@click.command()
@click.option('--foo-arg', default='asdf')
def foo(foo_arg):
    click.echo(f"Hello from foo with arg {foo_arg}")


@click.command()
@click.option('--bar-arg', default='zxcv')
def bar(bar_arg):
    click.echo(f"Hello from bar with arg {bar_arg}")


@click.command()
@click.option('--mode', type=click.Choice(["foo", "bar"]))
def cli(mode):
    if mode == "foo":
        foo()

    if mode == "bar":
        bar()


if __name__ == '__main__':
    cli()

В этом примере foo() и bar() следует рассматривать как модули, которые скрыты в репо и которыеможет также потребоваться большое количество опций CLI, которые я не хочу перегружать основной cli.py. Я чувствую, что параметры CLI foo() должны находиться в пределах foo по контекстным причинам. Вот как я хочу, чтобы это работало.

Пример 1:

python -m cli --mode foo --foo-arg asdf

должен выдать

Hello from foo with arg asdf

Пример 2:

python -m cli --mode bar --bar-arg zxcv

должен произвести

Hello from bar with arg zxcv

Пример 3:

python -m cli --mode foo --bar-arg qwer

должен произойти сбой, так как foo() не имеет опции --bar-arg.

Отказ от ответственности: я знаючтобы я мог зарегистрировать foo и bar как отдельные команды (вызывается через python -m cli foo --foo-arg asd, то есть с foo вместо --foo). Однако по причине, выходящей за рамки этого вопроса, мне нужно указать foo или bar с помощью идентификатора опции --mode. Это ограничение инструмента, взаимодействующего с моим приложением, которое, к сожалению, находится вне моего контроля.

Есть ли способ проанализировать аргументы и сделать возможным поток управления на основе подмножества аргументов, а затем передать оставшиесядля последующего модуля, не определяя параметры каждого модуля в качестве декораторов на def cli()?

Ответы [ 2 ]

1 голос
/ 03 ноября 2019

Использование опции для вызова подкоманды может быть достигнуто с помощью click.MultiCommand и пользовательского метода parse_args(), например:

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

def mode_opts_cmds(mode_opt_name, namespace=None):

    class RemoteCommandsAsModeOpts(click.MultiCommand):

        def __init__(self, *args, **kwargs):
            super(RemoteCommandsAsModeOpts, self).__init__(*args, **kwargs)
            self.mode_opt_name = '--{}'.format(mode_opt_name)
            opt = next(p for p in self.params if p.name == mode_opt_name)
            assert isinstance(opt.type, click.Choice)
            choices = set(opt.type.choices)
            self.commands = {k: v for k, v in (
                namespace or globals()).items() if k in choices}
            for command in self.commands.values():
                assert isinstance(command, click.Command)

        def parse_args(self, ctx, args):
            try:
                args.remove(self.mode_opt_name)
            except ValueError:
                pass
            super(RemoteCommandsAsModeOpts, self).parse_args(ctx, args)

        def list_commands(self, ctx):
            return sorted(self.commands)

        def get_command(self, ctx, name):
            return self.commands[name]

    return RemoteCommandsAsModeOpts

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

Чтобы использовать пользовательский класс, вызовите функцию mode_opts_cmds() для создания пользовательского класса, а затем используйте параметр cls, чтобы передать этот класс декоратору click.group().

@click.group(cls=mode_opts_cmds('mode'))
@click.option('--mode', type=click.Choice(["foo", "bar"]))
def cli(mode):
    """My wonderful cli"""

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

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

В этом случае мы перебираем click.Group.parse_args(), так что когда анализируется командная строка, мы просто удаляем --mode, а затем командная строка анализируется так же, как подкоманда normal .

Чтобы сделать это библиотечной функцией

Если пользовательский класс будет находиться вбиблиотеку, затем вместо использования значения по умолчанию globals() из файла библиотеки необходимо будет передать пространство имен. Затем необходимо вызвать метод создания пользовательского класса с чем-то вроде:

@click.group(cls=mode_opts_cmds('mode', namespace=globals()))

TestКод

import click

@click.command()
@click.option('--foo-arg', default='asdf')
def foo(foo_arg):
    """the foo command is ok"""
    click.echo(f"Hello from foo with arg {foo_arg}")


@click.command()
@click.option('--bar-arg', default='zxcv')
def bar(bar_arg):
    """bar is my favorite"""
    click.echo(f"Hello from bar with arg {bar_arg}")


@click.group(cls=mode_opts_cmds('mode'))
@click.option('--mode', type=click.Choice(["foo", "bar"]))
def cli(mode):
    """My wonderful cli command"""


if __name__ == "__main__":
    commands = (
        '--mode foo --foo-arg asdf',
        '--mode bar --bar-arg zxcv',
        '--mode foo --bar-arg qwer',
        '--mode foo --help',
        '',
    )

    import sys, time

    time.sleep(1)
    print('Click Version: {}'.format(click.__version__))
    print('Python Version: {}'.format(sys.version))
    for command in commands:
        try:
            time.sleep(0.1)
            print('-----------')
            print('> ' + command)
            time.sleep(0.1)
            cli(command.split())

        except BaseException as exc:
            if str(exc) != '0' and \
                    not isinstance(exc,
                                   (click.ClickException, SystemExit)):
                raise

Результаты теста

Click Version: 7.0
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> --mode foo --foo-arg asdf
Hello from foo with arg asdf
-----------
> --mode bar --bar-arg zxcv
Hello from bar with arg zxcv
-----------
> --mode foo --bar-arg qwer
Usage: test.py foo [OPTIONS]
Try "test.py foo --help" for help.

Error: no such option: --bar-arg
-----------
> --mode foo --help
Usage: test.py foo [OPTIONS]

  the foo command is ok

Options:
  --foo-arg TEXT
  --help          Show this message and exit.
-----------
> 
Usage: test.py [OPTIONS] COMMAND [ARGS]...

  My wonderful cli command

Options:
  --mode [foo|bar]
  --help            Show this message and exit.

Commands:
  bar  bar is my favorite
  foo  the foo command is ok                    
0 голосов
/ 03 ноября 2019

Вы можете использовать обратные вызовы для проверок: https://click.palletsprojects.com/en/7.x/options/#callbacks-for-validation

Кроме того, используйте команды с несколькими цепочками, поэтому каждый аргумент будет передан нужной команде.

https://click.palletsprojects.com/en/7.x/commands/#multi-command-chaining

...