необязательный подпарсер argparse (для --version)

У меня есть следующий код (с использованием Python 2.7):

# shared command line options, like --version or --verbose
parser_shared = argparse.ArgumentParser(add_help=False)
parser_shared.add_argument('--version', action='store_true')

# the main parser, inherits from `parser_shared`
parser = argparse.ArgumentParser(description='main', parents=[parser_shared])

# several subcommands, which can't inherit from the main parser, since
# it would expect subcommands ad infinitum
subparsers = parser.add_subparsers('db', parents=[parser_shared])

...

args = parser.parse_args()

Теперь я хотел бы иметь возможность вызывать эту программу, например. с добавлением --version к обычной программе или некоторой подкоманде:

$ prog --version
0.1

$ prog db --version
0.1

По сути, мне нужно объявить необязательные подпарсеры. Я знаю, что это действительно не поддерживается, но есть ли обходные пути или альтернативы?

Изменить: сообщение об ошибке, которое я получаю:

$ prog db --version
# works fine

$ prog --version
usage: ....
prog: error: too few arguments

person miku    schedule 15.12.2011    source источник


Ответы (7)


Согласно документации, --version с action='version' (а не с action='store_true') автоматически печатает номер версии:

parser.add_argument('--version', action='version', version='%(prog)s 2.0')
person eumiro    schedule 15.12.2011

FWIW, я тоже столкнулся с этим и в итоге «решил» его, не используя подпарсеры (у меня уже была собственная система для печати справки, поэтому я ничего там не потерял).

Вместо этого я делаю это:

parser.add_argument("command", nargs="?",
                    help="name of command to execute")

args, subcommand_args = parser.parse_known_args()

...и затем подкоманда создает свой собственный парсер (похожий на подпарсер), который работает только с subcommand_args.

person Matthew    schedule 26.02.2014

Кажется, это реализует основную идею необязательного подпарсера. Разбираем стандартные аргументы, применимые ко всем подкомандам. Затем, если что-то осталось, мы вызываем парсер на остальных. Первичные аргументы являются родителем подкоманды, поэтому -h отображается правильно. Я планирую ввести интерактивную подсказку, если нет подкоманд.

import argparse

p1 = argparse.ArgumentParser( add_help = False )    
p1.add_argument( ‘–flag1′ )

p2 = argparse.ArgumentParser( parents = [ p1 ] )
s = p2.add_subparsers()
p = s.add_parser( ‘group’ )
p.set_defaults( group=True )

( init_ns, remaining ) = p1.parse_known_args( )

if remaining:
    p2.parse_args( args = remaining, namespace=init_ns )
else:
    print( ‘Enter interactive loop’ )

print( init_ns )
person mikeporterinmd    schedule 04.04.2015

Как обсуждалось в http://bugs.python.org/issue9253 (argparse: необязательные подпарсеры), по состоянию на Python 3.3, подпарсеры теперь необязательны. Это был непреднамеренный результат изменения способа parse_args проверки обязательных аргументов.

Я нашел выдумку, которая восстанавливает предыдущее (требуемые подпарсеры) поведение, явно устанавливая атрибут required действия subparsers.

parser = ArgumentParser(prog='test')
subparsers = parser.add_subparsers()
subparsers.required = True   # the fudge
subparsers.dest = 'command'
subparser = subparsers.add_parser("foo", help="run foo")
parser.parse_args()

Подробнее см. в этом выпуске. Я ожидаю, что если и когда эта проблема будет должным образом исправлена, по умолчанию потребуются подпарсеры с какой-то опцией для установки атрибута required в False. Но есть большое количество argparse исправлений.

person hpaulj    schedule 27.02.2014


Пока мы ждем доставки этой функции, мы можем использовать такой код:

# Make sure that main is the default sub-parser
if '-h' not in sys.argv and '--help' not in sys.argv:
    if len(sys.argv) < 2:
        sys.argv.append('main')
    if sys.argv[1] not in ('main', 'test'):
        sys.argv = [sys.argv[0], 'main'] + sys.argv[1:]
person Joshua Richardson    schedule 13.11.2012
comment
Обратите внимание, что мы ждем эту базовую функцию с 2009 года. - person yac; 11.11.2013
comment
Я начал использовать docopt вместо встроенного парсера аргументов. Он поддерживает смешанное использование с действиями или без них, то есть с глаголами. docopt.org - person Joshua Richardson; 14.11.2013

Хотя ответ @eumiro касается опции --version, он может сделать это только потому, что это особый случай для optparse. Чтобы разрешить общие вызовы:

 prog
 prog --verbose
 prog --verbose main
 prog --verbose db 

и пусть prog --version работает так же, как prog --verbose mainprog main --verbose), вы можете добавить метод в Argumentparser и вызвать его с именем подпарсера по умолчанию, непосредственно перед вызовом parse_args():

import argparse
import sys

def set_default_subparser(self, name, args=None):
    """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 first position, this implies no
            # global options without a sub_parsers specified
            if args is None:
                sys.argv.insert(1, name)
            else:
                args.insert(0, name)

argparse.ArgumentParser.set_default_subparser = set_default_subparser

def do_main(args):
    print 'main verbose', args.verbose

def do_db(args):
    print 'db verbose:', args.verbose

parser = argparse.ArgumentParser()
parser.add_argument('--verbose', action='store_true')
parser.add_argument('--version', action='version', version='%(prog)s 2.0')
subparsers = parser.add_subparsers()
sp = subparsers.add_parser('main')
sp.set_defaults(func=do_main)
sp.add_argument('--verbose', action='store_true')
sp = subparsers.add_parser('db')
sp.set_defaults(func=do_db)

parser.set_default_subparser('main')
args = parser.parse_args()

if hasattr(args, 'func'):
    args.func(args)

Метод set_default_subparser() является частью пакета ruamel.std.argparse.

person Anthon    schedule 15.10.2014