Подпрограммы argparse с вложенными пространствами имен

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

Изменение: Этот пример, вероятно, не совсем то, что я должен делать, чтобы структурировать синтаксический анализатор для достижения моей цели, но это было то, что я до сих пор работал. Моя конкретная цель состоит в том, чтобы иметь возможность давать подпарамерам группы параметров, которые анализируются в полях пространства имен. У меня была идея с родителем просто использовать общие параметры для этой же цели.

Пример:

import argparse

# Main parser
main_parser = argparse.ArgumentParser()
main_parser.add_argument("-common")

# filter parser
filter_parser = argparse.ArgumentParser(add_help=False)
filter_parser.add_argument("-filter1")
filter_parser.add_argument("-filter2")

# sub commands
subparsers = main_parser.add_subparsers(help='sub-command help')

parser_a = subparsers.add_parser('command_a', help="command_a help", parents=[filter_parser])
parser_a.add_argument("-foo")
parser_a.add_argument("-bar")

parser_b = subparsers.add_parser('command_b', help="command_b help", parents=[filter_parser])
parser_b.add_argument("-biz")
parser_b.add_argument("-baz")

# parse
namespace = main_parser.parse_args()
print namespace

Это то, что я получаю, очевидно:

$ python test.py command_a -foo bar -filter1 val
Namespace(bar=None, common=None, filter1='val', filter2=None, foo='bar')

Но это то, что я действительно после:

Namespace(bar=None, common=None, foo='bar', 
          filter=Namespace(filter1='val', filter2=None))

И тогда еще больше групп опций уже проанализированы в пространствах имен:

Namespace(common=None, 
          foo='bar', bar=None,  
          filter=Namespace(filter1='val', filter2=None),
          anotherGroup=Namespace(bazers='val'),
          anotherGroup2=Namespace(fooers='val'),
          )

Я нашел связанный вопрос здесь, но он включает в себя некоторый пользовательский анализ и, кажется, охватывает только действительно специфические обстоятельства.

Есть ли вариант где-нибудь сказать argparse для анализа определенных групп в полях пространства имен?

Ответ 1

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

class GroupedAction(argparse.Action):    
    def __call__(self, parser, namespace, values, option_string=None):
        group,dest = self.dest.split('.',2)
        groupspace = getattr(namespace, group, argparse.Namespace())
        setattr(groupspace, dest, values)
        setattr(namespace, group, groupspace)

Существуют различные способы указания имени group. Он может быть передан в качестве аргумента при определении действия. Он может быть добавлен как параметр. Здесь я решил проанализировать его из dest (поэтому namespace.filter.filter1 может получить значение filter.filter1.

# Main parser
main_parser = argparse.ArgumentParser()
main_parser.add_argument("-common")

filter_parser = argparse.ArgumentParser(add_help=False)
filter_parser.add_argument("--filter1", action=GroupedAction, dest='filter.filter1', default=argparse.SUPPRESS)
filter_parser.add_argument("--filter2", action=GroupedAction, dest='filter.filter2', default=argparse.SUPPRESS)

subparsers = main_parser.add_subparsers(help='sub-command help')

parser_a = subparsers.add_parser('command_a', help="command_a help", parents=[filter_parser])
parser_a.add_argument("--foo")
parser_a.add_argument("--bar")
parser_a.add_argument("--bazers", action=GroupedAction, dest='anotherGroup.bazers', default=argparse.SUPPRESS)
...
namespace = main_parser.parse_args()
print namespace

Мне пришлось добавить default=argparse.SUPPRESS, поэтому запись bazers=None не отображается в основном пространстве имен.

Результат:

>>> python PROG command_a --foo bar --filter1 val --bazers val
Namespace(anotherGroup=Namespace(bazers='val'), 
    bar=None, common=None, 
    filter=Namespace(filter1='val'), 
    foo='bar')

Если вам нужны записи по умолчанию во вложенных пространствах имен, вы можете определить пространство имен перед этим:

filter_namespace = argparse.Namespace(filter1=None, filter2=None)
namespace = argparse.Namespace(filter=filter_namespace)
namespace = main_parser.parse_args(namespace=namespace)

результат по-прежнему, за исключением:

filter=Namespace(filter1='val', filter2=None)

Ответ 2

Я не совсем уверен, что вы спрашиваете, но я думаю, что вы хотите для группы аргументов или sub-command, чтобы поместить его аргументы в пространство под-имен.

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

parser = argparse.ArgumentParser()
parser.add_argument('--foo')
breakfast = parser.add_argument_group('breakfast')
breakfast.add_argument('--spam')
breakfast.add_argument('--eggs')
args = parser.parse_args()

Теперь список всех пунктов назначения для параметров breakfast:

[action.dest for action in breakfast._group_actions]

И пары ключ-значение в args:

args._get_kwargs()

Итак, все, что нам нужно - это переместить те, которые соответствуют. Будет немного легче, если мы создадим словари для создания пространств имен из:

breakfast_options = [action.dest for action in breakfast._group_actions]
top_names = {name: value for (name, value) in args._get_kwargs()
             if name not in breakfast_options}
breakfast_names = {name: value for (name, value) in args._get_kwargs()
                   if name in breakfast_options}
top_names['breakfast'] = argparse.Namespace(**breakfast_names)
top_namespace = argparse.Namespace(**top_names)

И что это; top_namespace выглядит следующим образом:

Namespace(breakfast=Namespace(eggs=None, spam='7'), foo='bar')

Конечно, в этом случае у нас есть одна статическая группа. Что, если вы хотите получить более общее решение? Легко. parser._action_groups - список всех групп, но первые два являются глобальными позиционными и ключевыми группами. Итак, просто перейдите по parser._action_groups[2:] и сделайте то же самое для каждого, что вы сделали для breakfast выше.


Как насчет подкоманд вместо групп? Аналогично, но детали разные. Если вы сохранили вокруг каждого объекта subparser, он просто весь другой ArgumentParser. Если нет, но вы сохранили объект subparsers, это особый тип Action, чей choices - это dict, ключи которого являются именами подпараметров и значениями которых являются сами подпарамеры. Если вы не сохранили ни того, ни другого, начинайте с parser._subparsers и выясняйте это оттуда.

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


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

Ответ 3

Вложение с подклассами Action отлично подходит для одного типа действий, но это неприятно, если вам нужно подклассифицировать несколько типов (хранить, сохранять true, добавлять и т.д.). Вот еще одна идея - подкласс Namespace. Сделайте одинаковое разделение имен и setattr, но сделайте это в пространстве имен, а не в действии. Затем просто создайте экземпляр нового класса и передайте его в parse_args.

class Nestedspace(argparse.Namespace):
    def __setattr__(self, name, value):
        if '.' in name:
            group,name = name.split('.',1)
            ns = getattr(self, group, Nestedspace())
            setattr(ns, name, value)
            self.__dict__[group] = ns
        else:
            self.__dict__[name] = value

p = argparse.ArgumentParser()
p.add_argument('--foo')
p.add_argument('--bar', dest='test.bar')
print(p.parse_args('--foo test --bar baz'.split()))

ns = Nestedspace()
print(p.parse_args('--foo test --bar baz'.split(), ns))
p.add_argument('--deep', dest='test.doo.deep')
args = p.parse_args('--foo test --bar baz --deep doodod'.split(), Nestedspace())
print(args)
print(args.test.doo)
print(args.test.doo.deep)

производства:

Namespace(foo='test', test.bar='baz')
Nestedspace(foo='test', test=Nestedspace(bar='baz'))
Nestedspace(foo='test', test=Nestedspace(bar='baz', doo=Nestedspace(deep='doodod')))
Nestedspace(deep='doodod')
doodod

__getattr__ для этого пространства имен (необходимого для действий, таких как count и append) может быть:

def __getattr__(self, name):
    if '.' in name:
        group,name = name.split('.',1)
        try:
            ns = self.__dict__[group]
        except KeyError:
            raise AttributeError
        return getattr(ns, name)
    else:
        raise AttributeError

Я предложил несколько других вариантов, но лучше всего. Он помещает данные о хранении, где они принадлежат, в пространстве имен, а не в синтаксический анализатор.

Ответ 4

Начиная с ответа abarnert, я собрал следующий MWE ++;-), который обрабатывает несколько групп конфигурации с похожими именами параметров.

#!/usr/bin/env python2
import argparse, re

cmdl_skel = {
    'description'       : 'An example of multi-level argparse usage.',
    'opts'              : {
        '--foo' : {
            'type'    : int,
            'default' : 0,
            'help'    : 'foo help main',
        },
        '--bar' : {
            'type'    : str,
            'default' : 'quux',
            'help'    : 'bar help main',
        },
    },
    # Assume your program uses sub-programs with their options. Argparse will
    # first digest *all* defs, so opts with the same name across groups are
    # forbidden. The trick is to use the module name (=> group.title) as
    # pseudo namespace which is stripped off at group parsing
    'groups' : [
        {   'module'        : 'mod1',
            'description'   : 'mod1 description',
            'opts'          : {
                '--mod1-foo, --mod1.foo'  : {
                    'type'    : int,
                    'default' : 0,
                    'help'    : 'foo help for mod1'
                },
            },
        },
        {   'module'        : 'mod2',
            'description'   : 'mod2 description',
            'opts'          : {
                '--mod2-foo, --mod2.foo'  : {
                    'type'    : int,
                    'default' : 1,
                    'help'    : 'foo help for mod2'
                },
            },
        },
    ],
    'args'              : {
        'arg1'  : {
            'type'    : str,
            'help'    : 'arg1 help',
        },
        'arg2'  : {
            'type'    : str,
            'help'    : 'arg2 help',
        },
    }
}


def parse_args ():
    def _parse_group (parser, opt, **optd):
        # digest variants
        optv = re.split('\s*,\s*', opt)
        # this may rise exceptions...
        parser.add_argument(*optv, **optd)

    errors = {}
    parser = argparse.ArgumentParser(description=cmdl_skel['description'],
                formatter_class=argparse.ArgumentDefaultsHelpFormatter)

    # it'd be nice to loop in a single run over zipped lists, but they have
    # different lenghts...
    for opt in cmdl_skel['opts'].keys():
        _parse_group(parser, opt, **cmdl_skel['opts'][opt])

    for arg in cmdl_skel['args'].keys():
        _parse_group(parser, arg, **cmdl_skel['args'][arg])

    for grp in cmdl_skel['groups']:
        group = parser.add_argument_group(grp['module'], grp['description'])
        for mopt in grp['opts'].keys():
            _parse_group(group, mopt, **grp['opts'][mopt])

    args = parser.parse_args()

    all_group_opts = []
    all_group_names = {}
    for group in parser._action_groups[2:]:
        gtitle = group.title
        group_opts = [action.dest for action in group._group_actions]
        all_group_opts += group_opts
        group_names = {
            # remove the leading pseudo-namespace
            re.sub("^%s_" % gtitle, '', name) : value
                for (name, value) in args._get_kwargs()
                    if name in group_opts
        }
        # build group namespace
        all_group_names[gtitle] = argparse.Namespace(**group_names)

    # rebuild top namespace
    top_names = {
        name: value for (name, value) in args._get_kwargs()
            if name not in all_group_opts
    }
    top_names.update(**all_group_names)
    top_namespace = argparse.Namespace(**top_names)

    return top_namespace


def main():
    args = parse_args()

    print(str(args))
    print(args.bar)
    print(args.mod1.foo)


if __name__ == '__main__':
    main()

Затем вы можете вызвать его так (мнемоника: --mod1-... являются параметрами для "mod1" и т.д.):

$ ./argparse_example.py one two --bar=three --mod1-foo=11231 --mod2.foo=46546
Namespace(arg1='one', arg2='two', bar='three', foo=0, mod1=Namespace(foo=11231), mod2=Namespace(foo=46546))
three
11231

Ответ 5

В этом script я изменил метод __call__ для argparse._SubParsersAction. Вместо того, чтобы передавать namespace на подпараметр, он передает новый. Затем он добавляет это к основному namespace. Я изменяю только 3 строки __call__.

import argparse

def mycall(self, parser, namespace, values, option_string=None):
    parser_name = values[0]
    arg_strings = values[1:]

    # set the parser name if requested
    if self.dest is not argparse.SUPPRESS:
        setattr(namespace, self.dest, parser_name)

    # select the parser
    try:
        parser = self._name_parser_map[parser_name]
    except KeyError:
        args = {'parser_name': parser_name,
                'choices': ', '.join(self._name_parser_map)}
        msg = _('unknown parser %(parser_name)r (choices: %(choices)s)') % args
        raise argparse.ArgumentError(self, msg)

    # CHANGES
    # parse all the remaining options into a new namespace
    # store any unrecognized options on the main namespace, so that the top
    # level parser can decide what to do with them
    newspace = argparse.Namespace()
    newspace, arg_strings = parser.parse_known_args(arg_strings, newspace)
    setattr(namespace, 'subspace', newspace) # is there a better 'dest'?

    if arg_strings:
        vars(namespace).setdefault(argparse._UNRECOGNIZED_ARGS_ATTR, [])
        getattr(namespace, argparse._UNRECOGNIZED_ARGS_ATTR).extend(arg_strings)

argparse._SubParsersAction.__call__ = mycall

# Main parser
main_parser = argparse.ArgumentParser()
main_parser.add_argument("--common")

# sub commands
subparsers = main_parser.add_subparsers(dest='command')

parser_a = subparsers.add_parser('command_a')
parser_a.add_argument("--foo")
parser_a.add_argument("--bar")

parser_b = subparsers.add_parser('command_b')
parser_b.add_argument("--biz")
parser_b.add_argument("--baz")

# parse
input = 'command_a --foo bar --bar val --filter extra'.split()
namespace = main_parser.parse_known_args(input)
print namespace

input = '--common test command_b --biz bar --baz val'.split()
namespace = main_parser.parse_args(input)
print namespace

Это дает:

(Namespace(command='command_a', common=None, 
    subspace=Namespace(bar='val', foo='bar')), 
['--filter', 'extra'])

Namespace(command='command_b', common='test', 
    subspace=Namespace(baz='val', biz='bar'))

Я использовал parse_known_args для проверки того, как дополнительные строки передаются обратно в основной синтаксический анализатор.

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

argument_groups тоже не помогают. Они используются форматом справки, но не parse_args.

Я мог бы подклассом _SubParsersAction (вместо переназначения __call__), но тогда я бы изменил main_parse.register.

Ответ 6

Обратите внимание на модуль argpext на PyPi, это может вам помочь!

Ответ 7

Основываясь на ответе @abarnert, я написал простую функцию, которая делает то, что хочет ОП:

from argparse import Namespace, ArgumentParser


def parse_args(parser):
    assert isinstance(parser, ArgumentParser)
    args = parser.parse_args()

    # the first two argument groups are 'positional_arguments' and 'optional_arguments'
    pos_group, optional_group = parser._action_groups[0], parser._action_groups[1]
    args_dict = args._get_kwargs()
    pos_optional_arg_names = [arg.dest for arg in pos_group._group_actions] + [arg.dest for arg in optional_group._group_actions]
    pos_optional_args = {name: value for name, value in args_dict if name in pos_optional_arg_names}
    other_group_args = dict()

    # If there are additional argument groups, add them as nested namespaces
    if len(parser._action_groups) > 2:
        for group in parser._action_groups[2:]:
            group_arg_names = [arg.dest for arg in group._group_actions]
            other_group_args[group.title] = Namespace(**{name: value for name, value in args_dict if name in group_arg_names})

    # combine the positiona/optional args and the group args
    combined_args = pos_optional_args
    combined_args.update(other_group_args)
    return Namespace(**combined_args)

Вы просто даете ему экземпляр ArgumentParser и он возвращает вложенное NameSpace соответствии с групповой структурой аргументов.