15

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 将某些组解析为命名空间字段?

4

7 回答 7

16

如果重点只是将选定的参数放入它们自己的参数中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)
于 2013-09-07T20:12:29.470 回答
9

我不完全确定您要问什么,但我认为您想要的是参数组子命令将其参数放入子命名空间。

据我所知,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并弄清楚。

无论如何,一旦您知道如何找到要移动的名称以及要移动它们的位置,这与使用组相同。


如果除了全局参数和/或组以及子解析器特定的参数和/或组之外,还有一些由多个子解析器共享的组......那么从概念上讲它会变得很棘手,因为每个子解析器最终都会引用相同的组,并且您不能将其移至所有组。但幸运的是,您只处理一个子解析器(或没有),因此您可以忽略其他子解析器并移动所选子解析器下的任何共享组(以及所选子解析器中不存在的任何组,要么离开在顶部,或者扔掉,或者任意选择一个子解析器)。

于 2013-09-07T00:51:52.383 回答
8

使用子类嵌套Action对于一种类型的 Action 来说很好,但如果您需要子类化几种类型(store、store true、append 等),那就麻烦了。这是另一个想法 - 子类命名空间。执行相同的名称拆分和 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__命名空间(计数和追加等操作需要)可以是:

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

我提出了其他几种选择,但最好是这样。它将存储详细信息放在它们所属的位置,在命名空间中,而不是解析器中。

于 2013-09-10T02:30:55.020 回答
1

在这个脚本中,我修改__call__了 argparse._SubParsersAction 的方法。它没有将 on传递namespace给子解析器,而是传递了一个新的。然后将其添加到 main 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.

于 2013-09-07T04:18:28.903 回答
1

从 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
于 2014-05-14T15:08:30.977 回答
0

根据@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

于 2019-06-17T12:41:08.657 回答
-1

请查看PyPi上的argpext模块,它可能会对您有所帮助!

于 2013-10-14T01:23:01.537 回答