Подкоманда по умолчанию или обработка без подкоманды с помощью argparse

Как я могу иметь подкоманду по умолчанию или обрабатывать случай, когда подкоманда не дается, используя argparse?

import argparse

a = argparse.ArgumentParser()
b = a.add_subparsers()
b.add_parser('hi')
a.parse_args()

Здесь я бы хотел, чтобы была выбрана команда или аргументы, которые должны обрабатываться только на основе следующего самого высокого уровня синтаксического анализатора (в данном случае синтаксического анализатора верхнего уровня).

 joiner @ X: ~ / src> python3 default_subcommand.py
использование: default_subcommand.py [-h] {привет} ...
default_subcommand.py: ошибка: слишком мало аргументов 

10 ответов

Решение

Кажется, я сам наткнулся на решение.

Если команда является необязательной, это делает команду опцией. В моей исходной конфигурации парсера у меня был package команда, которая может предпринять ряд возможных шагов, или она выполнит все шаги, если ничего не было дано. Это делает шаг выбором:

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...

На Python 3.2 (и 2.7) вы получите эту ошибку, но не на 3.3 и 3.4 (без ответа). Поэтому на 3.3/3.4 вы можете проверить parsed_args быть пустым Namespace,

Более общее решение состоит в том, чтобы добавить метод set_default_subparser() (берется из пакета https://bitbucket.org/ruamel/std.argparse) и вызывает этот метод непосредственно перед 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()

Это будет работать с 2.6 (если argparse устанавливается из PyPI), 2.7, 3.2, 3.3, 3.4. И позволяет сделать оба

python3 default_subcommand.py

а также

python3 default_subcommand.py hi

с тем же эффектом.

Позволяет выбрать новый подпарсер по умолчанию вместо одного из существующих.

Первая версия кода позволяет установить один из ранее определенных подпарсеров как один по умолчанию. Следующая модификация позволяет добавить новый подпаратор по умолчанию, который затем может быть использован для специальной обработки случая, когда подпарсер не был выбран пользователем (разные строки отмечены в коде)

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)

Параметр "по умолчанию" все равно не будет отображаться в справке:

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

Однако теперь стало возможным различать и обрабатывать отдельно вызов одного из предоставленных подпарсеров и вызов подпарамера по умолчанию, когда аргумент не был предоставлен:

$ python test_parser.py hi
Namespace(cmd='hi')
$ python test_parser.py 
Namespace(cmd='hey')

Вот хороший способ добавления 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
        )

Вы можете продублировать действие по умолчанию определенного подпапера в главном анализаторе, фактически сделав его по умолчанию.

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

Не работает с add_subparsers(required=True)вот почему if args.func там внизу

Может быть, то, что вы ищете, это dest аргумент add_subparsers:

(Предупреждение: работает в Python 3.4, но не в 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)

Теперь вы можете просто использовать значение cmd:

if cmd in [None, 'hi']:
    print('command "hi"')

В моем случае проще всего было явно указать имя подкоманды parse_args() когда argv был пуст.

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

Пример работает:

$ ./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')

Вы можете добавить аргумент со значением по умолчанию, которое будет использоваться, когда ничего не установлено, я полагаю.

Смотрите это: http://docs.python.org/dev/library/argparse.html

Редактировать:

Извините, я прочитал ваш вопрос немного быстро.

Я не думаю, что у вас был бы прямой способ делать то, что вы хотите через argparse. Но вы можете проверить длину sys.argv и, если она равна 1 (только имя скрипта), вы можете вручную передать параметры по умолчанию для разбора, выполнив что-то вроде этого:

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

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

Вот еще одно решение, использующее вспомогательную функцию для создания списка известных подкоманд:

      import argparse


def parse_args(argv):
    parser = argparse.ArgumentParser()

    commands = []
    subparsers = parser.add_subparsers(dest='command')

    def add_command(name, *args, **kwargs):
        commands.append(name)
        return subparsers.add_parser(name, *args, **kwargs)

    hi = add_command("hi")
    hi.add_argument('--name')
    add_command("hola")

    # check for default command
    if not argv or argv[0] not in commands:
        argv.insert(0, "hi")

    return parser.parse_args(argv)


assert parse_args([]).command == 'hi'
assert parse_args(['hi']).command == 'hi'
assert parse_args(['hi', '--name', 'John']).command == 'hi'
assert parse_args(['hi', '--name', 'John']).name == 'John'
assert parse_args(['--name', 'John']).command == 'hi'
assert parse_args(['hola']).command == 'hola'

В Python 2.7 вы можете переопределить поведение ошибки в подклассе (позор, что нет лучшего способа дифференцировать ошибку):

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

Для дальнейшего использования:

...
b = a.add_subparsers(dest='cmd')
b.set_defaults(cmd='hey')  # <-- this makes hey as default

b.add_parser('hi')

Итак, эти два будут одинаковыми:

  • python main.py эй
  • python main.py
Другие вопросы по тегам