Sous-commande par défaut, ou ne traitant aucune sous-commande avec argparse
-
28-10-2019 - |
Question
Comment puis-je avoir une sous-commande par défaut,ou gérer le cas où aucune sous-commande n'est donnée en utilisant argparse
?
import argparse
a = argparse.ArgumentParser()
b = a.add_subparsers()
b.add_parser('hi')
a.parse_args()
Ici, je voudrais qu'une commande soit sélectionnée, ou les arguments à gérer uniquement en fonction du niveau supérieur d'analyseur suivant (dans ce cas, l'analyseur de niveau supérieur).
joiner@X:~/src> python3 default_subcommand.py usage: default_subcommand.py [-h] {hi} ... default_subcommand.py: error: too few arguments
La solution 2
Il semble que j'ai fini par trouver la solution moi-même.
Si la commande est facultative, alors cela fait de la commande une option .Dans ma configuration originale d'analyseur, j'avais une commande package
qui pouvait prendre une gamme d'étapes possibles, ou elle effectuerait toutes les étapes si aucune n'était donnée.Cela fait de l'étape un choix:
parser = argparse.ArgumentParser()
command_parser = subparsers.add_parser('command')
command_parser.add_argument('--step', choices=['prepare', 'configure', 'compile', 'stage', 'package'])
...other command parsers
parsed_args = parser.parse_args()
if parsed_args.step is None:
do all the steps...
Autres conseils
Sur Python 3.2 (et 2.7), vous obtiendrez cette erreur, mais pas sur 3.3 et 3.4 (pas de réponse). Par conséquent, sur la version 3.3 / 3.4, vous pouvez tester le parsed_args
comme étant un Namespace
vide.
Une solution plus générale consiste à ajouter une méthode set_default_subparser()
(tirée du ruamel.std.argparse package) et appelez cette méthode juste avant parse_args()
:
import argparse
import sys
def set_default_subparser(self, name, args=None, positional_args=0):
"""default subparser selection. Call after setup, just before parse_args()
name: is the name of the subparser to call by default
args: if set is the argument list handed to parse_args()
, tested with 2.7, 3.2, 3.3, 3.4
it works with 2.6 assuming argparse is installed
"""
subparser_found = False
for arg in sys.argv[1:]:
if arg in ['-h', '--help']: # global help if no subparser
break
else:
for x in self._subparsers._actions:
if not isinstance(x, argparse._SubParsersAction):
continue
for sp_name in x._name_parser_map.keys():
if sp_name in sys.argv[1:]:
subparser_found = True
if not subparser_found:
# insert default in last position before global positional
# arguments, this implies no global options are specified after
# first positional argument
if args is None:
sys.argv.insert(len(sys.argv) - positional_args, name)
else:
args.insert(len(args) - positional_args, name)
argparse.ArgumentParser.set_default_subparser = set_default_subparser
def do_hi():
print('inside hi')
a = argparse.ArgumentParser()
b = a.add_subparsers()
sp = b.add_parser('hi')
sp.set_defaults(func=do_hi)
a.set_default_subparser('hi')
parsed_args = a.parse_args()
if hasattr(parsed_args, 'func'):
parsed_args.func()
Cela fonctionnera avec 2.6 (si argparse
est installé à partir de PyPI), 2.7, 3.2, 3.3, 3.4. Et vous permet de faire les deux
python3 default_subcommand.py
et
python3 default_subcommand.py hi
avec le même effet.
Permettre de choisir un nouveau sous-analyseur par défaut, au lieu de l'un des existants.
La première version du code permet de définir l'un des sous-analyseurs précédemment définis par défaut. La modification suivante permet d'ajouter un nouveau sous-analyseur par défaut, qui pourrait ensuite être utilisé pour traiter spécifiquement le cas où aucun sous-analyseur n'a été sélectionné par l'utilisateur (différentes lignes marquées dans le code)
def set_default_subparser(self, name, args=None, positional_args=0):
"""default subparser selection. Call after setup, just before parse_args()
name: is the name of the subparser to call by default
args: if set is the argument list handed to parse_args()
, tested with 2.7, 3.2, 3.3, 3.4
it works with 2.6 assuming argparse is installed
"""
subparser_found = False
existing_default = False # check if default parser previously defined
for arg in sys.argv[1:]:
if arg in ['-h', '--help']: # global help if no subparser
break
else:
for x in self._subparsers._actions:
if not isinstance(x, argparse._SubParsersAction):
continue
for sp_name in x._name_parser_map.keys():
if sp_name in sys.argv[1:]:
subparser_found = True
if sp_name == name: # check existance of default parser
existing_default = True
if not subparser_found:
# If the default subparser is not among the existing ones,
# create a new parser.
# As this is called just before 'parse_args', the default
# parser created here will not pollute the help output.
if not existing_default:
for x in self._subparsers._actions:
if not isinstance(x, argparse._SubParsersAction):
continue
x.add_parser(name)
break # this works OK, but should I check further?
# insert default in last position before global positional
# arguments, this implies no global options are specified after
# first positional argument
if args is None:
sys.argv.insert(len(sys.argv) - positional_args, name)
else:
args.insert(len(args) - positional_args, name)
argparse.ArgumentParser.set_default_subparser = set_default_subparser
a = argparse.ArgumentParser()
b = a.add_subparsers(dest ='cmd')
sp = b.add_parser('hi')
sp2 = b.add_parser('hai')
a.set_default_subparser('hey')
parsed_args = a.parse_args()
print(parsed_args)
L'option "par défaut" n'apparaîtra toujours pas dans l'aide:
python test_parser.py -h
usage: test_parser.py [-h] {hi,hai} ...
positional arguments:
{hi,hai}
optional arguments:
-h, --help show this help message and exit
Cependant, il est maintenant possible de différencier et de gérer séparément l'appel de l'un des sous-analyseurs fournis et l'appel du sous-analyseur par défaut lorsqu'aucun argument n'a été fourni:
$ python test_parser.py hi
Namespace(cmd='hi')
$ python test_parser.py
Namespace(cmd='hey')
Voici une meilleure façon d'ajouter une méthode set_default_subparser
:
class DefaultSubcommandArgParse(argparse.ArgumentParser):
__default_subparser = None
def set_default_subparser(self, name):
self.__default_subparser = name
def _parse_known_args(self, arg_strings, *args, **kwargs):
in_args = set(arg_strings)
d_sp = self.__default_subparser
if d_sp is not None and not {'-h', '--help'}.intersection(in_args):
for x in self._subparsers._actions:
subparser_found = (
isinstance(x, argparse._SubParsersAction) and
in_args.intersection(x._name_parser_map.keys())
)
if subparser_found:
break
else:
# insert default in first position, this implies no
# global options without a sub_parsers specified
arg_strings = [d_sp] + arg_strings
return super(DefaultSubcommandArgParse, self)._parse_known_args(
arg_strings, *args, **kwargs
)
Peut-être que vous cherchez l'argument dest
de add_subparsers
:
( Attention: fonctionne en Python 3.4, mais pas en 2.7 )
import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='cmd')
parser_hi = subparsers.add_parser('hi')
parser.parse_args([]) # Namespace(cmd=None)
Vous pouvez maintenant utiliser simplement la valeur de cmd
:
if cmd in [None, 'hi']:
print('command "hi"')
Vous pouvez ajouter un argument avec une valeur par défaut qui sera utilisée lorsque rien n'est défini, je crois.
Voir ceci: http://docs.python.org/dev/library/argparse.html#default
Modifier:
Désolé, j'ai lu votre question un peu rapidement.
Je ne pense pas que vous ayez un moyen direct de faire ce que vous voulez via argparse.Mais vous pouvez vérifier la longueur de sys.argv et si sa longueur est de 1 (uniquement le nom du script), vous pouvez passer manuellement les paramètres par défaut pour l'analyse, en faisant quelque chose comme ceci:
import argparse
a = argparse.ArgumentParser()
b = a.add_subparsers()
b.add_parser('hi')
if len(sys.argv) == 1:
a.parse_args(['hi'])
else:
a.parse_args()
Je pense que cela devrait faire ce que vous voulez, mais je suis d'accord que ce serait bien de l'avoir prêt à l'emploi.
En python 2.7, vous pouvez remplacer le comportement d'erreur dans une sous-classe (dommage qu'il n'y ait pas de meilleure façon de différencier l'erreur):
import argparse
class ExceptionArgParser(argparse.ArgumentParser):
def error(self, message):
if "invalid choice" in message:
# throw exception (of your choice) to catch
raise RuntimeError(message)
else:
# restore normal behaviour
super(ExceptionArgParser, self).error(message)
parser = ExceptionArgParser()
subparsers = parser.add_subparsers(title='Modes', dest='mode')
default_parser = subparsers.add_parser('default')
default_parser.add_argument('a', nargs="+")
other_parser = subparsers.add_parser('other')
other_parser.add_argument('b', nargs="+")
try:
args = parser.parse_args()
except RuntimeError:
args = default_parser.parse_args()
# force the mode into namespace
setattr(args, 'mode', 'default')
print args
Vous pouvez dupliquer l'action par défaut d'un sous-analyseur spécifique sur l'analyseur principal, ce qui en fait la valeur par défaut.
import argparse
p = argparse.ArgumentParser()
sp = p.add_subparsers()
a = sp.add_parser('a')
a.set_defaults(func=do_a)
b = sp.add_parser('b')
b.set_defaults(func=do_b)
p.set_defaults(func=do_b)
args = p.parse_args()
if args.func:
args.func()
else:
parser.print_help()
Ne fonctionne pas avec add_subparsers(required=True)
, c'est pourquoi le if args.func
est là-bas.
Dans mon cas, j'ai trouvé plus facile de fournir explicitement le nom de la sous-commande à parse_args()
lorsque argv
était vide.
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(help='commands')
runParser = subparsers.add_parser('run', help='[DEFAULT ACTION]')
altParser = subparsers.add_parser('alt', help='Alternate command')
altParser.add_argument('alt_val', type=str, help='Value required for alt command.')
# Here's my shortcut: If `argv` only contains the script name,
# manually inject our "default" command.
args = parser.parse_args(['run'] if len(sys.argv) == 1 else None)
print args
Exemple d'exécutions:
$ ./test.py
Namespace()
$ ./test.py alt blah
Namespace(alt_val='blah')
$ ./test.py blah
usage: test.py [-h] {run,alt} ...
test.py: error: invalid choice: 'blah' (choose from 'run', 'alt')
Pour référence ultérieure:
...
b = a.add_subparsers(dest='cmd')
b.set_defaults(cmd='hey') # <-- this makes hey as default
b.add_parser('hi')
donc, ces deux seront identiques:
- python main.py hey
- python main.py