En Python, en utilisant argparse, n'autorisez que les entiers positifs

164

Le titre résume assez bien ce que j'aimerais qu'il se passe.

Voici ce que j'ai, et bien que le programme n'explose pas sur un entier non positif, je veux que l'utilisateur soit informé qu'un entier non positif est fondamentalement insensé.

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

Et la sortie:

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

Sortie avec un négatif:

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

Maintenant, évidemment, je pourrais simplement ajouter un si pour déterminer if args.gamesest négatif, mais j'étais curieux de savoir s'il y avait un moyen de le piéger au argparseniveau, afin de profiter de l'impression d'utilisation automatique.

Idéalement, il imprimerait quelque chose de similaire à ceci:

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'

Ainsi:

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'

Pour l'instant, je fais ça, et je suppose que je suis content:

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

Réponses:

244

Cela devrait être possible en utilisant type. Vous devrez toujours définir une méthode réelle qui décidera de ceci pour vous:

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)

Ceci est fondamentalement juste un exemple adapté de la perfect_squarefonction dans les docs sur argparse.

Yuushi
la source
1
Votre fonction peut-elle avoir plusieurs valeurs? Comment ça marche?
Tom
2
Si la conversion intéchoue, y aura-t-il toujours une sortie lisible? Ou devriez-vous effectuer try raisela conversion manuellement pour cela?
NOhs
4
@MrZ Ça va donner quelque chose comme error: argument foo: invalid check_positive value: 'foo=<whatever>'. Vous pourriez simplement ajouter un try:... except ValueError:autour de celui-ci qui soulève à nouveau une exception avec un meilleur message d'erreur.
Yuushi du
59

type serait l'option recommandée pour gérer les conditions / vérifications, comme dans la réponse de Yuushi.

Dans votre cas spécifique, vous pouvez également utiliser le choicesparamètre si votre limite supérieure est également connue:

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

Remarque: utilisez rangeplutôt que xrangepour python 3.x

anéroïde
la source
3
J'imagine que ce serait assez inefficace, car vous généreriez une plage puis la parcouriez pour valider votre entrée. Un rapide ifest beaucoup plus rapide.
TravisThomas
2
@ trav1th En effet, c'est peut-être le cas, mais c'est un exemple d'utilisation de la documentation. De plus, j'ai dit dans ma réponse que la réponse de Yuushi était la bonne. Bon pour donner des options. Et dans le cas d'argparse, cela se produit une fois par exécution, utilise un générateur ( xrange) et ne nécessite pas de code supplémentaire. Ce compromis est disponible. A chacun de décider de la voie à suivre.
anéroïde
16
Pour être plus clair sur le point de jgritty sur la réponse de ben auteur, les choix = xrange (0,1000) entraîneront l'écriture de la liste entière des entiers de 1 à 999 inclus dans votre console chaque fois que vous utilisez --help ou si un argument invalide est à condition de. Pas un bon choix dans la plupart des cas.
biomiker
9

Le moyen rapide et sale, si vous avez un maximum prévisible ainsi que un minimum pour votre argument, est de l'utiliser choicesavec une plage

parser.add_argument('foo', type=int, choices=xrange(0, 1000))
Ben auteur
la source
24
L'inconvénient est le résultat hideux.
jgritty
6
accent sur sale , je suppose.
ben auteur le
4
Pour être plus clair sur le point de jgritty, les choix = xrange (0,1000) entraîneront l'écriture de la liste entière des entiers de 1 à 999 inclus dans votre console chaque fois que vous utilisez --help ou si un argument non valide est fourni. Pas un bon choix dans la plupart des cas.
biomiker
8

Une alternative plus simple, en particulier en cas de sous-classement argparse.ArgumentParser, consiste à lancer la validation depuis l'intérieur de la parse_argsméthode.

À l'intérieur d'une telle sous-classe:

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

Cette technique n'est peut-être pas aussi cool qu'un appelable personnalisé, mais elle fait le travail.


À propos ArgumentParser.error(message):

Cette méthode imprime un message d'utilisation comprenant le message d'erreur standard et met fin au programme avec un code d'état de 2.


Crédit: réponse de jonatan

Acumenus
la source
Ou à tout le moins, remplacer print "-g/--games: must be positive."; sys.exit(1)par juste parser.error("-g/--games: must be positive."). (Utilisation comme dans la réponse de Jonatan .)
anéroïde
3

Dans le cas où quelqu'un (comme moi) rencontre cette question dans une recherche Google, voici un exemple de la façon d'utiliser une approche modulaire pour résoudre proprement le problème plus général de n'autoriser les entiers argparse que dans une plage spécifiée :

# 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")

Cela vous permet de faire quelque chose comme:

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 foon'autorise désormais que les entiers positifs , comme l'OP demandé.

Notez qu'en plus des formulaires ci-dessus, un maximum est également possible avec IntRange:

parser.add_argument('other', type=IntRange(imax=10))  # Must have other <= 10
Pallgeuer
la source