web-dev-qa-db-fra.com

Cliquez sur Interfaces de ligne de commande: définissez les options requises si une autre option facultative n'est pas définie

Lors de l'écriture d'une interface de ligne de commande (CLI) avec le Python bibliothèque de clic , est-il possible de définir par exemple trois options où la deuxième et la troisième ne sont nécessaires que si le premier (facultatif) n'a pas été réglé?

Mon cas d'utilisation est un système de connexion qui me permet de m'authentifier via un authentication token (option 1), ou, alternativement, via username (option 2) et password (option 3).

Si le jeton a été donné, il n'est pas nécessaire de vérifier que username et password sont définis ou les invitent. Sinon, si le jeton a été omis, username et password deviennent obligatoires et doivent être fournis.

Cela peut-il être fait d'une manière ou d'une autre en utilisant des rappels?

Mon code pour commencer qui bien sûr ne reflète pas le modèle prévu:

@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()
14
Dirk

Cela peut être fait en créant une classe personnalisée dérivée de click.Option, Et dans cette classe en chevauchant la méthode click.Option.handle_parse_result() comme:

Classe personnalisée:

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)

Utilisation d'une classe personnalisée:

Pour utiliser la classe personnalisée, passez le paramètre cls à click.option Décorateur comme:

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

Comment cela marche-t-il?

Cela fonctionne car click est un framework OO bien conçu. Le décorateur @click.option() instancie généralement un objet click.Option Mais permet de remplacer ce comportement par le cls paramètre. Il est donc relativement facile d'hériter de click.Option dans notre propre classe et de contourner les méthodes souhaitées.

Dans ce cas, nous roulons sur click.Option.handle_parse_result() et désactivons la nécessité de user/password Si le jeton authentication-token Est présent, et nous nous plaignons si les deux user/password Sont authentication-token sont présents.

Remarque: Cette réponse a été inspirée par cette réponse

Code de test:

@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())

Résultats:

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

t:None  u:name  p:pword

de 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.
21
Stephen Rauch

Légèrement amélioré réponse de Stephen Rauch pour avoir plusieurs paramètres mutex.

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)

utiliser comme ceci:

@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...")
4
masi