带有嵌套命名空间的 argparse 子命令
Posted
技术标签:
【中文标题】带有嵌套命名空间的 argparse 子命令【英文标题】:argparse subcommands with nested namespaces 【发布时间】:2013-09-11 04:06:06 【问题描述】: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'),
)
我找到了related question here,但它涉及一些自定义解析,似乎只涵盖了一个非常具体的情况。
是否有一个选项可以告诉 argparse 将某些组解析为命名空间字段?
【问题讨论】:
我不确定您希望它如何工作。正如您所写,filter1
和 filter2
位于***解析器中,而不是位于某些名为 filter
的子解析器中。 argparse 怎么知道你希望它充当每个子解析器的子解析器,而实际上它不是?
@abarnert:我可能应该根据您的问题重新格式化我的示例。因为正如你所指出的那样,我放在一起的结构确实不合适。我的目标确实是能够将选项组应用于子解析器,并将它们解析到命名空间中。如果它们可以通用就好了,这就是我尝试使用父结构的原因。
所以你正在寻找类似pip
、git
等的东西,除了***全局选项和特定于每个子命令的选项外,还有共享选项通过多个不同的子命令(例如,pip
的 --verbose
、--upgrade
和 --user
选项),并且能够直接表示该共享而不是使其隐式(通过将选项组复制到多个子解析器)?
或者您只是想要 add_argument_group
所做的事情(并且您可以复制组),除了您希望分组的参数出现在结果中的子命名空间中?因为使用后处理器很容易做到这一点:为每个组创建一个子命名空间,迭代主命名空间,以及作为组成员的每个参数,将其移动到子命名空间。但是,如果你也需要的话,使用子解析器会更复杂一些。
@abarnert:是的,你是对的。我应该使用一个参数组,并在事后进行后期处理。感谢您的回答!
【参考方案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)
【讨论】:
是的。我将接受这个而不是之前接受的答案,因为这确实使用 argparse 的功能(自定义操作)解决了我的目标。实际上……点符号“dest”正是我最初所希望的。谢谢! 我对您的 GroupedAction 做了一些补充,让它清理***原始属性,并且还可以选择从选项中派生组/字段:pastebin.com/qgQBBuvP @jdi:这正是我所说的通过子类化自定义解析来扩展 argparse 可能更好的意思;我只展示了如何做,因为你的问题暗示你不想这样做。我同意这是一个很好的答案。 @abarnert:我不认为我曾经暗示我不想要一个子类化的选项。实际上恰恰相反。我问的是使用内置设施,子类化确实可以满足。因为它直接与现有的解析逻辑一起工作。尽管我发现这种方法总体上导致了更多的工作,因为当您执行一个自定义操作时,您必须做更多的事情,处理发生的事件、布尔常量等。但它正在工作。 不如创建一个自定义命名空间类,它采用dest
和group.dest
一样,并创建所需的嵌套对象?定义的命名空间类非常简单。只要您的新课程与getattr
、hasattr
和setattr
一起使用,它就可以更精彩。【参考方案2】:
我不完全确定你在问什么,但我认为你想要的是 argument group 或 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
是一个字典,其键是子解析器名称,其值是子解析器本身。如果您两者都不保留……从parser._subparsers
开始并从那里弄清楚。
无论如何,一旦您知道如何找到要移动的名称以及要移动它们的位置,这与组相同。
如果除了全局参数和/或组以及子解析器特定的参数和/或组之外,还有一些由多个子解析器共享的组……那么从概念上讲它会变得很棘手,因为每个子解析器最终都有引用到同一个组,你不能把它移到他们所有人。但幸运的是,您只处理一个子解析器(或没有),因此您可以忽略其他子解析器并移动所选子解析器下的任何共享组(以及 不存在的任何组在选定的子解析器中,要么留在顶部,要么扔掉,或者任意选择一个子解析器。
【讨论】:
是的,这几乎回答了我的问题。我首先从查看组开始,它似乎只根据帮助对它们进行分组,开箱即用。因此,您已经解释说它确实需要一些手动后期处理,这很好。只需要看一个这样的例子,表明它是 argparse 所需的方法。谢谢! @jdi:正如我在回答中所说,我认为通过子类化而不是后处理来扩展 argparse 可能在这里更容易。组对象是您可以轻松构建以做更多事情的东西,而对解析器对象几乎没有更改。这可能也更惯用。但无论您觉得哪个更舒服,都可能没问题。【参考方案3】:嵌套Action
子类对于一种Action 来说很好,但是如果您需要对几种类型(store、store true、append 等)进行子类化,那就麻烦了。这是另一个想法 - 子类命名空间。执行相同的名称拆分和 setattr,但在 Namespace 而不是 Action 中执行。然后只需创建一个新类的实例,并将其传递给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__
(计数和追加等操作需要)可以是:
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
我提出了其他几个选项,但最好是这样。它将存储详细信息放在它们所属的位置,在命名空间中,而不是解析器中。
【讨论】:
太酷了。我什至没有考虑将命名空间子类化。总的来说,我喜欢这个,但是自从你上次回答以来,我发现了一些好处,比如将 ArgumentGroup 之类的子类化以设置与该字段匹配的默认元变量,并注册自定义操作。我确信这个自定义命名空间组合起来很有意义。【参考方案4】:在这个脚本中,我修改了 argparse._SubParsersAction 的 __call__
方法。它没有将namespace
传递给子解析器,而是传递一个新的。然后它将它添加到主要的namespace
。我只改了__call__
的3行。
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
。
【讨论】:
这是一个很酷的例子,说明如何通过猴子补丁对其进行解析......虽然缺点是它使用已知/未知的 args 方法,这意味着过滤器没有记录或通过 argparse 管理.【参考方案5】:从 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
【讨论】:
【参考方案6】:根据@abarnert 的回答,我编写了一个简单的函数来满足 OP 的要求:
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
。
【讨论】:
【参考方案7】:请查看PyPi 上的argpext module,它可能会对您有所帮助!
【讨论】:
以上是关于带有嵌套命名空间的 argparse 子命令的主要内容,如果未能解决你的问题,请参考以下文章