Argparse: argumento obligatorio 'y' si 'x' está presente

118

Tengo un requisito de la siguiente manera:

./xyifier --prox --lport lport --rport rport

para el argumento prox, uso action = 'store_true' para verificar si está presente o no. No necesito ninguno de los argumentos. Pero, si --prox se establece que requiero rport y lport también. ¿Existe una manera fácil de hacer esto con argparse sin escribir codificación condicional personalizada?

Más código:

non_int.add_argument('--prox', action='store_true', help='Flag to turn on proxy')
non_int.add_argument('--lport', type=int, help='Listen Port.')
non_int.add_argument('--rport', type=int, help='Proxy port.')
asudhak
fuente
Conectando, pero quería mencionar mi biblioteca joffrey . Le permite hacer lo que quiere esta pregunta, por ejemplo, sin tener que validar todo usted mismo (como en la respuesta aceptada) o confiar en un truco de lagunas legales (como en la segunda respuesta más votada).
MI Wright
Para cualquiera que llegue aquí, otra solución increíble: stackoverflow.com/a/44210638/6045800
Tomerikoo

Respuestas:

120

No, no hay ninguna opción en argparse para hacer conjuntos de opciones mutuamente inclusivos .

La forma más sencilla de lidiar con esto sería:

if args.prox and (args.lport is None or args.rport is None):
    parser.error("--prox requires --lport and --rport.")
borntyping
fuente
2
Eso es lo que terminé haciendo
asudhak
20
Gracias por el parser.errormétodo, ¡esto es lo que estaba buscando!
MarSoft
7
¿No deberías usar 'o'? después de todo, necesita ambos if args.prox and (args.lport is None or args.rport is None):
argumentos
1
En lugar de args.lport is None, simplemente puede usar not args.lport. Creo que es un poco más pitónico.
CGFoX
7
Que impediría el establecimiento --lporto --rportque 0, lo que podría ser una entrada válida para el programa.
Borntyping
53

Estás hablando de tener argumentos requeridos condicionalmente. Como dijo @borntyping, puede verificar el error y hacerlo parser.error(), o simplemente puede aplicar un requisito relacionado con el --proxmomento en que agrega un nuevo argumento.

Una solución simple para su ejemplo podría ser:

non_int.add_argument('--prox', action='store_true', help='Flag to turn on proxy')
non_int.add_argument('--lport', required='--prox' in sys.argv, type=int)
non_int.add_argument('--rport', required='--prox' in sys.argv, type=int)

De esta forma requiredrecibe Trueo Falsedependiendo de si el usuario lo utiliza --prox. Esto también garantiza que -lporty -rporttengan un comportamiento independiente entre sí.

Mira
fuente
8
Tenga en cuenta que ArgumentParserse puede utilizar para analizar argumentos de una lista distinta de sys.argv, en cuyo caso esto fallaría.
BallpointBen
Además, esto fallará si --prox=<value>se usa la sintaxis.
fnkr
11

¿Qué tal si usamos el parser.parse_known_args()método y luego agregamos los argumentos --lporty los --rportargumentos requeridos si --proxestá presente?

# just add --prox arg now
non_int = argparse.ArgumentParser(description="stackoverflow question", 
                                  usage="%(prog)s [-h] [--prox --lport port --rport port]")
non_int.add_argument('--prox', action='store_true', 
                     help='Flag to turn on proxy, requires additional args lport and rport')
opts, rem_args = non_int.parse_known_args()
if opts.prox:
    non_int.add_argument('--lport', required=True, type=int, help='Listen Port.')
    non_int.add_argument('--rport', required=True, type=int, help='Proxy port.')
    # use options and namespace from first parsing
    non_int.parse_args(rem_args, namespace = opts)

También tenga en cuenta que puede proporcionar el espacio de nombres opts generado después del primer análisis mientras analiza los argumentos restantes la segunda vez. De esa manera, al final, después de que se haya realizado todo el análisis, tendrá un solo espacio de nombres con todas las opciones.

Inconvenientes:

  • Si --proxno está presente, las otras dos opciones dependientes ni siquiera están presentes en el espacio de nombres. Aunque se basa en su caso de uso, si --proxno está presente, lo que sucede con las otras opciones es irrelevante.
  • Necesita modificar el mensaje de uso ya que el analizador no conoce la estructura completa
  • --lporty --rportno aparezcas en el mensaje de ayuda
Aditya Sriram
fuente
5

¿Lo usa lportcuando proxno está configurado? Si no, ¿por qué no hacer lporty rportargumentar prox? p.ej

parser.add_argument('--prox', nargs=2, type=int, help='Prox: listen and proxy ports')

Eso les ahorra a sus usuarios escribir. Es tan fácil de probar if args.prox is not None:como if args.prox:.

hpaulj
fuente
1
Para completar el ejemplo, cuando su nargs es> 1, obtendrá una lista en el argumento analizado, etc., que puede abordar de la manera habitual. Por ejemplo, a,b = args.prox, a = args.prox[0], etc.
Dannid
1

¡La respuesta aceptada funcionó muy bien para mí! Dado que todo el código está roto sin pruebas, así es como probé la respuesta aceptada. parser.error()no genera un argparse.ArgumentErrorerror, sino que sale del proceso. Tienes que probar SystemExit.

con pytest

import pytest
from . import parse_arguments  # code that rasises parse.error()


def test_args_parsed_raises_error():
    with pytest.raises(SystemExit):
        parse_arguments(["argument that raises error"])

con unittests

from unittest import TestCase
from . import parse_arguments  # code that rasises parse.error()

class TestArgs(TestCase):

    def test_args_parsed_raises_error():
        with self.assertRaises(SystemExit) as cm:
            parse_arguments(["argument that raises error"])

inspirado en: Uso de unittest para probar argparse - errores de salida

Daniel Butler
fuente