Haga clic en Interfaces de línea de comando: haga que las opciones sean necesarias si otra opción opcional no está establecida

Al escribir una interfaz de línea de comandos (CLI) con la biblioteca de clics de Python, ¿es posible definir, por ejemplo, tres opciones donde la segunda y la tercera solo son necesarias si la primera (opcional) se dejó sin configurar?

Mi caso de uso es un sistema de inicio de sesión que me permite autenticarme a través de un authentication token (opción 1) o, alternativamente, a través del username (opción 2) y la password (opción 3).

Si se entregó el token, no es necesario verificar el username de username y la password están definiendo o solicitando. De lo contrario, si se omitía el token, entonces se requieren un username y una password que deben ser dados.

¿Se puede hacer esto de alguna manera usando devoluciones de llamada?

Mi código para empezar, que por supuesto no refleja el patrón deseado:

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

Esto se puede hacer construyendo una clase personalizada derivada de click.Option , y en esa clase sobre el método click.Option.handle_parse_result() como:

Clase personalizada:

 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) 

Usando la clase personalizada:

Para usar la clase personalizada, pase el parámetro cls a click.option decorator como:

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

¿Como funciona esto?

Esto funciona porque hacer clic es un marco OO bien diseñado. El @click.option() generalmente click.Option una instancia de un objeto click.Option pero permite que este comportamiento se anule con el parámetro cls . Por lo tanto, es un asunto relativamente fácil de heredar del click.Option en nuestra propia clase y sobrepasar los métodos deseados.

En este caso, sobrepasamos click.Option.handle_parse_result() y deshabilitamos la necesidad de user/password si authentication-token token está presente, y nos quejamos si tanto el user/password como user/password son authentication-token .

Nota: esta respuesta fue inspirada por esta respuesta

Código de prueba:

 @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:%su:%sp:%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()) 

Resultados:

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

 t:None u:name p:pword 

desde el login('--help'.split()) 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. 

Mejoró ligeramente la respuesta de Stephen Rauch para tener múltiples parámetros de exclusión mutua.

 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) 

utilizar así:

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