Щелкните Интерфейсы командной строки: сделайте параметры обязательными, если другие дополнительные параметры не установлены.

При написании интерфейса командной строки (CLI) с Python библиотекой кликов можно ли определить, например, три варианта, где второй и третий требуются только в том случае, если первый (необязательный) не установлен?

Мой вариант использования — это система входа в систему, которая позволяет мне аутентифицироваться либо через authentication token (вариант 1), либо, альтернативно, через username (вариант 2) и password (вариант 3).

Если токен был предоставлен, нет необходимости проверять определение username и password или запрашивать их. В противном случае, если токен был опущен, то username и password становятся обязательными и должны быть указаны.

Можно ли это как-то сделать с помощью обратных вызовов?

Мой код для начала работы, который, конечно, не отражает предполагаемый шаблон:

@click.command()
@click.option('--authentication-token', prompt=True, required=True)
@click.option('--username', prompt=True, required=True)
@click.option('--password', hide_input=True, prompt=True, required=True)
def login(authentication_token, username, password):
    print(authentication_token, username, password)

if __name__ == '__main__':
    login()

person Dirk    schedule 29.05.2017    source источник


Ответы (3)


Это можно сделать, создав собственный класс, производный от click.Option, и в этом классе переопределить метод click.Option.handle_parse_result(), например:

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

import click

class NotRequiredIf(click.Option):
    def __init__(self, *args, **kwargs):
        self.not_required_if = kwargs.pop('not_required_if')
        assert self.not_required_if, "'not_required_if' parameter required"
        kwargs['help'] = (kwargs.get('help', '') +
            ' NOTE: This argument is mutually exclusive with %s' %
            self.not_required_if
        ).strip()
        super(NotRequiredIf, self).__init__(*args, **kwargs)

    def handle_parse_result(self, ctx, opts, args):
        we_are_present = self.name in opts
        other_present = self.not_required_if in opts

        if other_present:
            if we_are_present:
                raise click.UsageError(
                    "Illegal usage: `%s` is mutually exclusive with `%s`" % (
                        self.name, self.not_required_if))
            else:
                self.prompt = None

        return super(NotRequiredIf, self).handle_parse_result(
            ctx, opts, args)

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

Чтобы использовать пользовательский класс, передайте параметр cls декоратору click.option, например:

@click.option('--username', prompt=True, cls=NotRequiredIf,
              not_required_if='authentication_token')

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

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

В этом случае мы игнорируем click.Option.handle_parse_result() и отключаем необходимость user/password, если присутствует токен authentication-token, и жалуемся, если присутствуют оба user/password и authentication-token.

Примечание. Этот ответ был вдохновлен этим ответом

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

@click.command()
@click.option('--authentication-token')
@click.option('--username', prompt=True, cls=NotRequiredIf,
              not_required_if='authentication_token')
@click.option('--password', prompt=True, hide_input=True, cls=NotRequiredIf,
              not_required_if='authentication_token')
def login(authentication_token, username, password):
    click.echo('t:%s  u:%s  p:%s' % (
        authentication_token, username, password))

if __name__ == '__main__':
    login('--username name --password pword'.split())
    login('--help'.split())
    login(''.split())
    login('--username name'.split())
    login('--authentication-token token'.split())

Результаты:

от login('--username name --password pword'.split()):

t:None  u:name  p:pword

от login('--help'.split()):

Usage: test.py [OPTIONS]

Options:
  --authentication-token TEXT
  --username TEXT              NOTE: This argument is mutually exclusive with
                               authentication_token
  --password TEXT              NOTE: This argument is mutually exclusive with
                               authentication_token
  --help                       Show this message and exit.
person Stephen Rauch    schedule 03.06.2017

Немного улучшен ответ Стивена Рауха, чтобы иметь несколько параметров мьютекса.

import click

class Mutex(click.Option):
    def __init__(self, *args, **kwargs):
        self.not_required_if:list = kwargs.pop("not_required_if")

        assert self.not_required_if, "'not_required_if' parameter required"
        kwargs["help"] = (kwargs.get("help", "") + "Option is mutually exclusive with " + ", ".join(self.not_required_if) + ".").strip()
        super(Mutex, self).__init__(*args, **kwargs)

    def handle_parse_result(self, ctx, opts, args):
        current_opt:bool = self.name in opts
        for mutex_opt in self.not_required_if:
            if mutex_opt in opts:
                if current_opt:
                    raise click.UsageError("Illegal usage: '" + str(self.name) + "' is mutually exclusive with " + str(mutex_opt) + ".")
                else:
                    self.prompt = None
        return super(Mutex, self).handle_parse_result(ctx, opts, args)

использовать так:

@click.group()
@click.option("--username", prompt=True, cls=Mutex, not_required_if=["token"])
@click.option("--password", prompt=True, hide_input=True, cls=Mutex, not_required_if=["token"])
@click.option("--token", cls=Mutex, not_required_if=["username","password"])
def login(ctx=None, username:str=None, password:str=None, token:str=None) -> None:
    print("...do what you like with the params you got...")
person omni    schedule 08.07.2018

Вот еще один вариант, в котором not_required_if должен быть указан с вариантом змеи_case, required используется, а не приглашение, и, что важно, он работает, если другие аргументы передаются через переменные среды, а не в командной строке с помощью ctx.command.get_params (...) и param.consume_value(...):

import click


class Mutex(click.Option):
    def __init__(self, *args, **kwargs):
        self.not_required_if: list = kwargs.pop("not_required_if")

        assert self.not_required_if, "'not_required_if' parameter required"
        kwargs["help"] = (kwargs.get("help", "") + "Option is mutually exclusive with " + ", ".join(self.not_required_if) + ".").strip()
        super(Mutex, self).__init__(*args, **kwargs)

    def handle_parse_result(self, ctx, opts, args):
        current_opt: bool = self.consume_value(ctx, opts)
        for other_param in ctx.command.get_params(ctx):
            if other_param is self:
                continue
            if other_param.human_readable_name in self.not_required_if:
                other_opt: bool = other_param.consume_value(ctx, opts)
                if other_opt:
                    if current_opt:
                        raise click.UsageError(
                            "Illegal usage: '" + str(self.name)
                            + "' is mutually exclusive with "
                            + str(other_param.human_readable_name) + "."
                        )
                    else:
                        self.required = None
        return super(Mutex, self).handle_parse_result(ctx, opts, args)
person Frankie Robertson    schedule 08.05.2020