Argparse:如果子解析器共享父级,则子解析器的选项会覆盖主解析器

Argparse: options for subparsers override main if both share parent

我将 argparse 与多个子解析器一起使用。我希望我的程序在 args 的任何地方都采用冗长选项,包括子解析器。

from argparse import ArgumentParser
p = ArgumentParser()
p.add_argument('--verbose', '-v', action='count')

sub = p.add_subparsers()
a = sub.add_parser('a')

print(p.parse_args())

默认情况下,主解析器的选项如果用于子解析器将抛出错误:

$ python tmp.py -v a
Namespace(verbose=1)

$ python tmp.py a -v
usage: tmp.py [-h] [--verbose] {a} ...
tmp.py: error: unrecognized arguments: -v

我研究了父解析器,来自 this answer

from argparse import ArgumentParser

parent = ArgumentParser(add_help=False)
parent.add_argument('--verbose', '-v', action='count')

main = ArgumentParser(parents=[parent])

sub = main.add_subparsers()
a = sub.add_parser('a', parents=[parent])

print(main.parse_args())

虽然出于某种原因,none 的共享标志在主解析器上工作。

$ python tmp2.py a -vvv
Namespace(verbose=3)
$ python tmp2.py -vvv a
Namespace(verbose=None)

请注意,主解析器肯定有适当的参数,因为当我将其更改为 main = ArgumentParser() 时,我得到 error: unrecognized arguments: -v。我在这里错过了什么?

首先,一些一般性评论。

主解析器处理输入直到调用子解析器,然后调用子解析器并给出剩余的 argv。完成后,它的 namespace 被合并回主 namespace.

parents 机制通过引用从 parent 复制操作。因此,您的主解析器和子解析器共享相同的 verbose Action 对象。当子解析器尝试设置不同的默认值或帮助时,这是一个问题。这可能不是问题,但请记住。

即使没有 parents 机制,在主解析器和子解析器之间共享 dest 或选项标志也可能很棘手。是否应该使用子解析器 Action 的默认值?如果两者都用呢?子解析器是否覆盖主解析器的操作?

最初主要 namespace 被传递给子解析器,它修改并返回。这在不久前已更改(如果需要,我可以找到 bug/issue)。现在子解析器以默认的空 namespace 开始,填充它。然后将这些值合并到 main.

因此,在您链接的 SO 问题中,请注意较旧的答案。 argparse 可能从那以后发生了变化。

我认为您的情况是主解析器和子解析器 verbose 分别计数。当你得到 None 时,你看到的是子解析器的默认值。

_Count_Action__call__ 是:

def __call__(self, parser, namespace, values, option_string=None):
    new_count = _ensure_value(namespace, self.dest, 0) + 1
    setattr(namespace, self.dest, new_count)

我怀疑在较旧的 argparse 中共享命名空间时,count 会累积,但如果不重新创建较旧的样式 subparser 操作,我无法对其进行测试class.

https://bugs.python.org/issue15327 - 最初的开发者建议给两个不同的参数dest。它记录了来自 main 和 sub 的输入。如果需要,您自己的代码可以合并结果。

https://bugs.python.org/issue27859argparse - subparsers does not retain namespace。在这里,我建议一种重新创建旧样式的方法。

https://bugs.python.org/issue9351 argparse set_defaults on subcommands should override top level set_defaults - 这是 2014 年更改命名空间使用的问题。

我对这种行为的解决方法(在@hpaulj 的回答中有很好的描述)是创建第二个解析器,它没有子解析器,只有第一次找到的位置参数。

第一个 parse_args,与第一个解析器一起使用,将验证位置参数和标志,在需要时显示错误消息或显示适当的帮助。

第二个 parse_args,对于第二个解析器,将正确填写命名空间。

以您的示例为基础:

from argparse import ArgumentParser

parent = ArgumentParser(add_help=False)
parent.add_argument('--verbose', '-v', action='count')

main1 = ArgumentParser(parents=[parent])
sub = main1.add_subparsers()

# eg: tmp.py -vv a -v
a = sub.add_parser('a', parents=[parent])
a.set_defaults(which='a')

# eg: tmp.py -vv v -v --output toto
b = sub.add_parser('b', parents=[parent])
b.add_argument('--output', type=str)
b.set_defaults(which='b')

args = main1.parse_args()
print(args)

# parse a second time with another parser
main2 = ArgumentParser(parents=[parent])
if args.which == 'a':
    main2.add_argument('a')
elif args.which == 'b':
    main2.add_argument('b')
    main2.add_argument('--output', type=str)

print(main2.parse_args())

给出:

$ ./tmp.py -vv a -v

Namespace(verbose=1, which='a')
Namespace(a='a', verbose=3)

$ ./tmp.py -vv b -v --output toto
Namespace(output='toto', verbose=1, which='b')
Namespace(b='b', output='toto', verbose=3)

$ ./tmp.py  -vv a  --output
usage: tmp.py [-h] [--verbose] {a,b} ...
tmp.py: error: unrecognized arguments: --output

我将此技术用于多个嵌套子解析器。