En Python, usando argparse, permita solo enteros positivos

164

El título resume bastante bien lo que me gustaría que sucediera.

Esto es lo que tengo, y aunque el programa no explota en un número entero no positivo, quiero que se informe al usuario que un número entero no positivo es básicamente una tontería.

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-g", "--games", type=int, default=162,
                    help="The number of games to simulate")
args = parser.parse_args()

Y la salida:

python simulate_many.py -g 20
Setting up...
Playing games...
....................

Salida con un negativo:

python simulate_many.py -g -2
Setting up...
Playing games...

Ahora, obviamente, podría agregar un if para determinar que if args.gameses negativo, pero tenía curiosidad por saber si había una manera de atraparlo en el argparsenivel, para aprovechar la impresión de uso automático.

Idealmente, imprimiría algo similar a esto:

python simulate_many.py -g a
usage: simulate_many.py [-h] [-g GAMES] [-d] [-l LEAGUE]
simulate_many.py: error: argument -g/--games: invalid int value: 'a'

Al igual que:

python simulate_many.py -g -2
usage: simulate_many.py [-h] [-g GAMES] [-d] [-l LEAGUE]
simulate_many.py: error: argument -g/--games: invalid positive int value: '-2'

Por ahora estoy haciendo esto, y creo que estoy feliz:

if args.games <= 0:
    parser.print_help()
    print "-g/--games: must be positive."
    sys.exit(1)
jgritty
fuente

Respuestas:

244

Esto debería ser posible utilizando type. Aún deberá definir un método real que decida esto por usted:

def check_positive(value):
    ivalue = int(value)
    if ivalue <= 0:
        raise argparse.ArgumentTypeError("%s is an invalid positive int value" % value)
    return ivalue

parser = argparse.ArgumentParser(...)
parser.add_argument('foo', type=check_positive)

Esto es básicamente un ejemplo adaptado de la perfect_squarefunción en los documentos en adelante argparse.

Yuushi
fuente
1
¿Puede su función tener múltiples valores? ¿Cómo funciona?
Tom
2
Si la conversión a intfalla, ¿seguirá habiendo una salida legible? ¿O debería try raisela conversión manualmente para eso?
NOhs
44
@MrZ Dará algo así error: argument foo: invalid check_positive value: 'foo=<whatever>'. Simplemente podría agregar un try:... except ValueError:alrededor que vuelva a generar una excepción con un mejor mensaje de error.
Yuushi
59

type sería la opción recomendada para manejar condiciones / controles, como en la respuesta de Yuushi.

En su caso específico, también puede usar el choicesparámetro si también se conoce su límite superior:

parser.add_argument('foo', type=int, choices=xrange(5, 10))

Nota: Use en rangelugar de xrangepara python 3.x

aneroide
fuente
3
Me imagino que esto sería bastante ineficiente, ya que generaría un rango y luego recorrerlo validaría su entrada. Un rápido ifes mucho más rápido.
TravisThomas
2
@ trav1th De hecho, podría ser, pero es un ejemplo de uso de los documentos. Además, he dicho en mi respuesta que la respuesta de Yuushi es la indicada. Bueno para dar opciones. Y en el caso de argparse, ocurre una vez por ejecución, usa un generador ( xrange) y no requiere código adicional. Esa compensación está disponible. Depende de cada uno decidir qué camino tomar.
aneroide
16
Para ser más claro sobre el punto de jgritty en la respuesta del autor ben, choices = xrange (0,1000) dará como resultado que se escriba en la consola toda la lista de enteros del 1 al 999 inclusive cada vez que use --help o si un argumento no válido es previsto. No es una buena opción en la mayoría de las circunstancias.
biomiker
9

La manera rápida y sucia, si tiene un máximo predecible y un mínimo para su arg, es usarlo choicescon un rango

parser.add_argument('foo', type=int, choices=xrange(0, 1000))
autor de ben
fuente
24
La desventaja es la salida horrible.
jgritty
66
énfasis en sucio , supongo.
autor de ben
44
Para ser más claro sobre el punto de jgritty, choices = xrange (0,1000) dará como resultado que se escriba en la consola toda la lista de enteros del 1 al 999 inclusive cada vez que use --help o si se proporciona un argumento no válido. No es una buena opción en la mayoría de las circunstancias.
biomiker
8

Una alternativa más simple, especialmente si se subclasifican argparse.ArgumentParser, es iniciar la validación desde el interior del parse_argsmétodo.

Dentro de tal subclase:

def parse_args(self, args=None, namespace=None):
    """Parse and validate args."""
    namespace = super().parse_args(args, namespace)
    if namespace.games <= 0:
         raise self.error('The number of games must be a positive integer.')
    return namespace

Puede que esta técnica no sea tan buena como una llamada personalizada, pero hace el trabajo.


Acerca de ArgumentParser.error(message):

Este método imprime un mensaje de uso que incluye el mensaje del error estándar y finaliza el programa con un código de estado de 2.


Crédito: respuesta por jonatan

Acumenus
fuente
O al menos, reemplazando print "-g/--games: must be positive."; sys.exit(1)con solo parser.error("-g/--games: must be positive."). (Uso como en la respuesta de jonatan .)
aneroide
3

En caso de que alguien (como yo) se encuentre con esta pregunta en una búsqueda en Google, aquí hay un ejemplo de cómo usar un enfoque modular para resolver perfectamente el problema más general de permitir enteros argparse solo en un rango específico :

# Custom argparse type representing a bounded int
class IntRange:

    def __init__(self, imin=None, imax=None):
        self.imin = imin
        self.imax = imax

    def __call__(self, arg):
        try:
            value = int(arg)
        except ValueError:
            raise self.exception()
        if (self.imin is not None and value < self.imin) or (self.imax is not None and value > self.imax):
            raise self.exception()
        return value

    def exception(self):
        if self.imin is not None and self.imax is not None:
            return argparse.ArgumentTypeError(f"Must be an integer in the range [{self.imin}, {self.imax}]")
        elif self.imin is not None:
            return argparse.ArgumentTypeError(f"Must be an integer >= {self.imin}")
        elif self.imax is not None:
            return argparse.ArgumentTypeError(f"Must be an integer <= {self.imax}")
        else:
            return argparse.ArgumentTypeError("Must be an integer")

Esto le permite hacer algo como:

parser = argparse.ArgumentParser(...)
parser.add_argument('foo', type=IntRange(1))     # Must have foo >= 1
parser.add_argument('bar', type=IntRange(1, 7))  # Must have 1 <= bar <= 7

La variable fooahora solo permite enteros positivos , como el OP solicitado.

Tenga en cuenta que, además de los formularios anteriores, solo es posible un máximo con IntRange:

parser.add_argument('other', type=IntRange(imax=10))  # Must have other <= 10
pallgeuer
fuente