web-dev-qa-db-fra.com

Argparse: comment gérer un nombre variable d'arguments (nargs = '*')

Je pensais que nargs='*' Était suffisant pour gérer un nombre variable d'arguments. Apparemment, ce n’est pas le cas et je ne comprends pas la cause de cette erreur.

Le code:

p = argparse.ArgumentParser()
p.add_argument('pos')
p.add_argument('foo')
p.add_argument('--spam', default=24, type=int, dest='spam')
p.add_argument('vars', nargs='*')

p.parse_args('1 2 --spam 8 8 9'.split())

Je pense que l'espace de noms résultant devrait être Namespace(pos='1', foo='2', spam='8', vars=['8', '9']). Au lieu de cela, argparse donne cette erreur:

usage: prog.py [-h] [--spam SPAM] pos foo [vars [vars ...]]
error: unrecognized arguments: 9 8

Fondamentalement, argparse ne sait pas où mettre ces arguments supplémentaires ... Pourquoi est-ce?

48
rubik

Le bogue pertinent Python est numéro 15112 .

argparse: nargs='*' l'argument positionnel n'accepte aucun élément s'il est précédé d'une option et d'un autre

Quand argparse analyse ['1', '2', '--spam', '8', '8', '9'] _ il essaie d’abord de faire correspondre ['1','2'] avec autant d’arguments de position que possible. Avec vos arguments, la chaîne de correspondance de modèle est AAA*: 1 argument pour pos et foo, et zéro argument pour vars (rappelez-vous * signifie ZERO_OR_MORE).

['--spam','8'] sont gérés par votre --spam argument. Puisque vars a déjà été défini sur [], il n'y a plus rien à gérer ['8','9'].

Le changement de programmation en argparse vérifie le cas où 0 Les chaînes d'arguments satisfont le modèle, mais il reste encore optionals à analyser. Il diffère ensuite le traitement de cette * argument.

Vous pourrez peut-être contourner cela en analysant d'abord l'entrée avec parse_known_args , puis gestion de remainder avec un autre appel à parse_args .

Pour avoir une totale liberté dans l’interpolation des options entre positions, dans numéro 14191 , je propose d’utiliser parse_known_args avec seulement le optionals, suivi d'un parse_args qui ne connaît que les positions. Le parse_intermixed_args La fonction que j’y ai postée pourrait être implémentée dans une sous-classe ArgumentParser, sans modifier le argparse.py le code lui-même.


Voici un moyen de gérer les sous-pêcheurs. J'ai pris le parse_known_intermixed_args fonction, la simplifie pour la présentation, puis en fait la parse_known_args fonction d’une sous-classe d’analyseur. J'ai dû faire un pas supplémentaire pour éviter la récursivité.

Finalement j'ai changé le _parser_class de l’action subparsers, chaque sous-fournisseur utilise donc cette alternative parse_known_args. Une alternative serait de sous-classe _SubParsersAction, en modifiant éventuellement son __call__.

from argparse import ArgumentParser

def parse_known_intermixed_args(self, args=None, namespace=None):
    # self - argparse parser
    # simplified from http://bugs.python.org/file30204/test_intermixed.py
    parsefn = super(SubParser, self).parse_known_args # avoid recursion

    positionals = self._get_positional_actions()
    for action in positionals:
        # deactivate positionals
        action.save_nargs = action.nargs
        action.nargs = 0

    namespace, remaining_args = parsefn(args, namespace)
    for action in positionals:
        # remove the empty positional values from namespace
        if hasattr(namespace, action.dest):
            delattr(namespace, action.dest)
    for action in positionals:
        action.nargs = action.save_nargs
    # parse positionals
    namespace, extras = parsefn(remaining_args, namespace)
    return namespace, extras

class SubParser(ArgumentParser):
    parse_known_args = parse_known_intermixed_args

parser = ArgumentParser()
parser.add_argument('foo')
sp = parser.add_subparsers(dest='cmd')
sp._parser_class = SubParser # use different parser class for subparsers
spp1 = sp.add_parser('cmd1')
spp1.add_argument('-x')
spp1.add_argument('bar')
spp1.add_argument('vars',nargs='*')

print parser.parse_args('foo cmd1 bar -x one 8 9'.split())
# Namespace(bar='bar', cmd='cmd1', foo='foo', vars=['8', '9'], x='one')
32
hpaulj

Solution simple: spécifiez le --spam drapeau avant de spécifier pos et foo:

p = argparse.ArgumentParser()
p.add_argument('pos')
p.add_argument('foo')
p.add_argument('--spam', default=24, type=int, dest='spam')
p.add_argument('vars', nargs='*')

p.parse_args('--spam 8 1 2 8 9'.split())

La même chose fonctionne si vous placez le --spam drapeau après avoir spécifié vos arguments variables.

p = argparse.ArgumentParser()
p.add_argument('pos')
p.add_argument('foo')
p.add_argument('--spam', default=24, type=int, dest='spam')
p.add_argument('vars', nargs='*')

p.parse_args('1 2 8 9 --spam 8'.split())

EDIT: Pour ce que cela vaut, il semble que changer le * à un + corrigera également l'erreur.

p = argparse.ArgumentParser()
p.add_argument('pos')
p.add_argument('foo')
p.add_argument('--spam', default=24, type=int, dest='spam')
p.add_argument('vars', nargs='+')

p.parse_args('1 2 --spam 8 8 9'.split())
11
caleb531