Подкоманды 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, он может вам помочь!