Python argparse: haga al menos un argumento requerido

92

He estado usando argparsepara un programa de Python que puede -process, -uploado ambos:

parser = argparse.ArgumentParser(description='Log archiver arguments.')
parser.add_argument('-process', action='store_true')
parser.add_argument('-upload',  action='store_true')
args = parser.parse_args()

El programa no tiene sentido sin al menos un parámetro. ¿Cómo puedo configurar argparsepara forzar la elección de al menos un parámetro?

ACTUALIZAR:

Siguiendo los comentarios: ¿Cuál es la forma Pythonic de parametrizar un programa con al menos una opción?

Adam Matan
fuente
9
-xes universalmente una bandera y opcional. Corta el -si es necesario.
1
¿No podría realizar processel comportamiento predeterminado (sin la necesidad de especificar ninguna opción) y permitir que el usuario lo cambie uploadsi esa opción está configurada? Por lo general, las opciones deben ser opcionales, de ahí el nombre. Deben evitarse las opciones obligatorias (esto también se encuentra en los argparse documentos).
Tim Pietzcker
@AdamMatan Han pasado casi tres años desde que hiciste tu pregunta, pero me gustó el desafío que se esconde en ella y aproveché la ventaja de que hay nuevas soluciones disponibles para este tipo de tareas.
Jan Vlcinsky

Respuestas:

107
if not (args.process or args.upload):
    parser.error('No action requested, add -process or -upload')
Phihag
fuente
1
Esa es probablemente la única forma, si argparseno tiene una opción incorporada para esto.
Adam Matan
29
args = vars(parser.parse_args())
if not any(args.values()):
    parser.error('No arguments provided.')
brentlance
fuente
3
+1 para una solución generalizada. También como el uso de vars(), que también es útil para pasar opciones cuidadosamente nombradas a un constructor con **.
Lenna
Que es exactamente lo que estoy haciendo con él. ¡Gracias!
brentlance
1
Diablos, eso me gusta vars. Simplemente lo hice .__dict__y me sentí tonto antes.
Theo Belaire
1
grandes respuestas. Tanto "vars" como "any" eran nuevos para mí :-)
Vivek Jha
21

Si no es la parte 'o ambas' (inicialmente me perdí esto), podría usar algo como esto:

parser = argparse.ArgumentParser(description='Log archiver arguments.')
parser.add_argument('--process', action='store_const', const='process', dest='mode')
parser.add_argument('--upload',  action='store_const', const='upload', dest='mode')
args = parser.parse_args()
if not args.mode:
    parser.error("One of --process or --upload must be given")

Sin embargo, probablemente sería una mejor idea usar subcomandos en su lugar.

Jacek Konieczny
fuente
4
Creo que quiere permitir --processOR --upload, no XOR. Esto evita que ambas opciones se configuren al mismo tiempo.
phihag
+1 porque mencionaste subcomandos. Sin embargo - como alguien señaló en los comentarios -xy --xxxson típicamente parámetros opcionales.
mac
20

Sé que esto es viejo como la suciedad, pero la forma de requerir una opción pero prohibir más de una (XOR) es así:

parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-process', action='store_true')
group.add_argument('-upload',  action='store_true')
args = parser.parse_args()
print args

Salida:

>opt.py  
usage: multiplot.py [-h] (-process | -upload)  
multiplot.py: error: one of the arguments -process -upload is required  

>opt.py -upload  
Namespace(process=False, upload=True)  

>opt.py -process  
Namespace(process=True, upload=False)  

>opt.py -upload -process  
usage: multiplot.py [-h] (-process | -upload)  
multiplot.py: error: argument -process: not allowed with argument -upload  
Knut
fuente
3
Desafortunadamente, el OP no quiere un XOR. Es uno o ambos, pero no ninguno, por lo que su último caso de prueba no cumple con sus requisitos.
kdopen
2
@kdopen: el encuestado aclaró que se trata de una variación de la pregunta original, que me pareció útil: "la forma de requerir una opción pero prohibir más de una" Quizás la etiqueta de Stack Exchange requiera una nueva pregunta en su lugar . Sin embargo, tener presente esta respuesta aquí me ha ayudado ...
erik.weathers
2
Esta publicación no responde a la pregunta inicial
Marc
2
¿Cómo responde esto a la pregunta de "al menos uno"?
xaxxon
2
Desafortunadamente, el OP no quiere un XOR.
duckman_1991
8

Revisión de requisitos

  • usar argparse(ignoraré este)
  • permitir que se invoquen una o dos acciones (se requiere al menos una).
  • intentar por Pythonic (prefiero llamarlo "POSIX" -como)

También hay algunos requisitos implícitos cuando se vive en la línea de comandos:

  • explicar el uso al usuario de una manera que sea fácil de entender
  • las opciones serán opcionales
  • permitir especificar banderas y opciones
  • permiten combinar con otros parámetros (como nombre de archivo o nombres).

Solución de muestra usando docopt(archivo managelog.py):

"""Manage logfiles
Usage:
    managelog.py [options] process -- <logfile>...
    managelog.py [options] upload -- <logfile>...
    managelog.py [options] process upload -- <logfile>...
    managelog.py -h

Options:
    -V, --verbose      Be verbose
    -U, --user <user>  Username
    -P, --pswd <pswd>  Password

Manage log file by processing and/or uploading it.
If upload requires authentication, you shall specify <user> and <password>
"""
if __name__ == "__main__":
    from docopt import docopt
    args = docopt(__doc__)
    print args

Intenta ejecutarlo:

$ python managelog.py
Usage:
    managelog.py [options] process -- <logfile>...
    managelog.py [options] upload -- <logfile>...
    managelog.py [options] process upload -- <logfile>...
    managelog.py -h

Muestre la ayuda:

$ python managelog.py -h
Manage logfiles
Usage:
    managelog.py [options] process -- <logfile>...
    managelog.py [options] upload -- <logfile>...
    managelog.py [options] process upload -- <logfile>...
    managelog.py -h

Options:
    -V, --verbose      Be verbose
    -U, --user <user>  Username
    -P, --pswd <pswd>  P    managelog.py [options] upload -- <logfile>...

Manage log file by processing and/or uploading it.
If upload requires authentication, you shall specify <user> and <password>

Y úsalo:

$ python managelog.py -V -U user -P secret upload -- alfa.log beta.log
{'--': True,
 '--pswd': 'secret',
 '--user': 'user',
 '--verbose': True,
 '-h': False,
 '<logfile>': ['alfa.log', 'beta.log'],
 'process': False,
 'upload': True}

Alternativa corta short.py

Puede haber una variante incluso más corta:

"""Manage logfiles
Usage:
    short.py [options] (process|upload)... -- <logfile>...
    short.py -h

Options:
    -V, --verbose      Be verbose
    -U, --user <user>  Username
    -P, --pswd <pswd>  Password

Manage log file by processing and/or uploading it.
If upload requires authentication, you shall specify <user> and <password>
"""
if __name__ == "__main__":
    from docopt import docopt
    args = docopt(__doc__)
    print args

El uso se ve así:

$ python short.py -V process upload  -- alfa.log beta.log
{'--': True,
 '--pswd': None,
 '--user': None,
 '--verbose': True,
 '-h': False,
 '<logfile>': ['alfa.log', 'beta.log'],
 'process': 1,
 'upload': 1}

Tenga en cuenta que, en lugar de valores booleanos para las claves de "proceso" y "carga", hay contadores.

Resulta que no podemos evitar la duplicación de estas palabras:

$ python short.py -V process process upload  -- alfa.log beta.log
{'--': True,
 '--pswd': None,
 '--user': None,
 '--verbose': True,
 '-h': False,
 '<logfile>': ['alfa.log', 'beta.log'],
 'process': 2,
 'upload': 1}

Conclusiones

En ocasiones, diseñar una buena interfaz de línea de comandos puede ser un desafío.

Hay varios aspectos del programa basado en la línea de comandos:

  • buen diseño de línea de comando
  • seleccionar / usar el analizador adecuado

argparse ofrece mucho, pero restringe los posibles escenarios y puede volverse muy complejo.

Con las docoptcosas van mucho más cortas conservando la legibilidad y ofreciendo un alto grado de flexibilidad. Si logra obtener argumentos analizados del diccionario y realiza algunas conversiones (a números enteros, abrir archivos ...) manualmente (o mediante otra biblioteca llamada schema), puede encontrar una docoptbuena opción para el análisis de línea de comandos.

Jan Vlcinsky
fuente
Nunca he oído hablar de docopt, ¡gran sugerencia!
Ton van den Heuvel
@TonvandenHeuvel Bueno. Solo quiero confirmar, todavía lo estoy usando como mi solución preferida para interfaces de línea de comandos.
Jan Vlcinsky
Mejor respuesta evar, gracias por los ejemplos detallados.
jnovack
5

Si necesita que un programa de Python se ejecute con al menos un parámetro, agregue un argumento que no tenga el prefijo de opción (- o - por defecto) y establezcanargs=+ (Se requiere un mínimo de un argumento). El problema que encontré con este método es que si no especificas el argumento, argparse generará un error de "muy pocos argumentos" y no imprimirá el menú de ayuda. Si no necesita esa funcionalidad, aquí se explica cómo hacerlo en el código:

import argparse

parser = argparse.ArgumentParser(description='Your program description')
parser.add_argument('command', nargs="+", help='describe what a command is')
args = parser.parse_args()

Yo creo que cuando se agrega una discusión con los prefijos de opción, nargs gobierna todo el analizador argumento y no sólo la opción. (Lo que quiero decir es que, si tiene una --optionbandera con nargs="+", la --optionbandera espera al menos un argumento. Si tiene optioncon nargs="+", espera al menos un argumento en general).

NuclearPeon
fuente
Podría agregar algo choices=['process','upload']a ese argumento.
hpaulj
5

Para http://bugs.python.org/issue11588 , estoy explorando formas de generalizar el mutually_exclusive_groupconcepto para manejar casos como este.

Con este desarrollo argparse.py, https://github.com/hpaulj/argparse_issues/blob/nested/argparse.py puedo escribir:

parser = argparse.ArgumentParser(prog='PROG', 
    description='Log archiver arguments.')
group = parser.add_usage_group(kind='any', required=True,
    title='possible actions (at least one is required)')
group.add_argument('-p', '--process', action='store_true')
group.add_argument('-u', '--upload',  action='store_true')
args = parser.parse_args()
print(args)

que produce lo siguiente help:

usage: PROG [-h] (-p | -u)

Log archiver arguments.

optional arguments:
  -h, --help     show this help message and exit

possible actions (at least one is required):
  -p, --process
  -u, --upload

Esto acepta entradas como '-u', '-up', '--proc --up', etc.

Termina ejecutando una prueba similar a https://stackoverflow.com/a/6723066/901925 , aunque el mensaje de error debe ser más claro:

usage: PROG [-h] (-p | -u)
PROG: error: some of the arguments process upload is required

Me pregunto:

  • ¿Son los parámetros lo kind='any', required=Truesuficientemente claros (acepte cualquiera del grupo; se requiere al menos uno)?

  • ¿Está (-p | -u)claro el uso ? Un grupo mutually_exclusive_group requerido produce lo mismo. ¿Existe alguna notación alternativa?

  • ¿Usar un grupo como este es más intuitivo que una phihag'ssimple prueba?

hpaulj
fuente
No puedo encontrar ninguna mención add_usage_groupen esta página: docs.python.org/2/library/argparse.html ; ¿Podría proporcionar un enlace a la documentación correspondiente?
P. Myer Nore
@ P.MyerNore, proporcioné un enlace, al comienzo de esta respuesta. Esto no se ha puesto en producción.
hpaulj
5

La mejor manera de hacer esto es usando el módulo incorporado de Python add_mutually_exclusive_group .

parser = argparse.ArgumentParser(description='Log archiver arguments.')
group = parser.add_mutually_exclusive_group()
group.add_argument('-process', action='store_true')
group.add_argument('-upload',  action='store_true')
args = parser.parse_args()

Si desea que solo se seleccione un argumento mediante la línea de comando, use required = True como argumento para el grupo

group = parser.add_mutually_exclusive_group(required=True)
faizan baig
fuente
2
¿Cómo consigue esto "al menos uno"? ¿No consigue "exactamente uno"?
xaxxon
3
Desafortunadamente, el OP no quiere un XOR. OP está buscando OR
duckman_1991
Esto no respondió la pregunta de OP, pero respondió la mía, así que gracias de todos modos ¯_ (ツ) _ / ¯
rosstex
2

¿Quizás usar sub-analizadores?

import argparse

parser = argparse.ArgumentParser(description='Log archiver arguments.')
subparsers = parser.add_subparsers(dest='subparser_name', help='sub-command help')
parser_process = subparsers.add_parser('process', help='Process logs')
parser_upload = subparsers.add_parser('upload', help='Upload logs')
args = parser.parse_args()

print("Subparser: ", args.subparser_name)

Ahora --helpmuestra:

$ python /tmp/aaa.py --help
usage: aaa.py [-h] {process,upload} ...

Log archiver arguments.

positional arguments:
  {process,upload}  sub-command help
    process         Process logs
    upload          Upload logs

optional arguments:
  -h, --help        show this help message and exit
$ python /tmp/aaa.py
usage: aaa.py [-h] {process,upload} ...
aaa.py: error: too few arguments
$ python3 /tmp/aaa.py upload
Subparser:  upload

También puede agregar opciones adicionales a estos sub-analizadores. Además, en lugar de usar eso dest='subparser_name', también puede vincular funciones para que se invoquen directamente en un subcomando dado (ver documentos).

jhutar
fuente
2

Esto logra el propósito y esto también se verá afectado en la --helpsalida autogenerada de argparse , que es en mi humilde opinión lo que quieren la mayoría de los programadores cuerdos (también funciona con argumentos opcionales):

parser.add_argument(
    'commands',
    nargs='+',                      # require at least 1
    choices=['process', 'upload'],  # restrict the choice
    help='commands to execute'
)

Documentos oficiales sobre esto: https://docs.python.org/3/library/argparse.html#choices

Beto
fuente
1

Use append_const para una lista de acciones y luego verifique que la lista esté completa:

parser.add_argument('-process', dest=actions, const="process", action='append_const')
parser.add_argument('-upload',  dest=actions, const="upload", action='append_const')

args = parser.parse_args()

if(args.actions == None):
    parser.error('Error: No actions requested')

Incluso puede especificar los métodos directamente dentro de las constantes.

def upload:
    ...

parser.add_argument('-upload',  dest=actions, const=upload, action='append_const')
args = parser.parse_args()

if(args.actions == None):
    parser.error('Error: No actions requested')

else:
    for action in args.actions:
        action()
storm_m2138
fuente