Подкоманды 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 для анализа определенных групп в полях пространства имен?

6 ответов

Решение

Если основное внимание уделяется просто помещению выбранных аргументов в свои собственные 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)

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

Насколько я знаю, 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 является диктовкой, чьи ключи являются именами подпарсеров, а значения - самими подпарсерами. Если вы не сохранили ни одного... начните с parser._subparsers и понять это оттуда.

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


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

Вложение с Action Подклассы хороши для одного типа Action, но неудобны, если вам нужно подклассы нескольких типов (store, store true, append и т. д.). Вот еще одна идея - подкласс 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

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

В этом сценарии я изменил __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,

Начиная с ответа 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

Based on the answer by @abarnert, I wrote a simple function that does what the OP wants:

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)

You just give it the ArgumentParser instance and it returns a nested NameSpace according to the group structure of the arguments.

Пожалуйста, проверьте модуль argpext на PyPi, он может вам помочь!

Другие вопросы по тегам