我有一个带有子解析器的
ArgumentParser
。有些标志对于所有子解析器都是通用的,我希望能够在子命令之前或之后指定它们,或者甚至在之前和之后混合(由用户自行决定)。
类似这样的:
$ ./test -v
Namespace(v=1)
$ ./test.py test -vv
Namespace(v=2)
$ ./test.py -vvv test
Namespace(v=3)
$ ./test.py -vv test -vv
Namespace(v=4)
所以我尝试了这样的事情:
#!/usr/bin/env python3
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-v", action="count")
subparsers = parser.add_subparsers()
sub = subparsers.add_parser("test")
sub.add_argument("-v", action="count")
print(parser.parse_args())
运行这个程序“test.py”给我:
./test.py -h
usage: test.py [-h] [-v] {test} ...
positional arguments:
{test}
options:
-h, --help show this help message and exit
-v
$ ./test.py test -h
usage: test.py test [-h] [-v]
options:
-h, --help show this help message and exit
-v
$ ./test.py -v
Namespace(v=1)
$ ./test.py test -vv
Namespace(v=2)
酷。但它也给了我:
$ ./test.py -vvv test
Namespace(v=None)
$ ./test.py -vv test -vv
Namespace(v=2)
不太酷:-(我还尝试显式指定父解析器:
common = argparse.ArgumentParser(add_help=False)
common.add_argument("-v", action="count")
parser = argparse.ArgumentParser(parents=[common])
sub = parser.add_subparsers().add_parser("test", parents=[common])
print(parser.parse_args())
但结果是一样的。所以,我猜一旦
test
子解析器启动,它就会将
v
的值重置为
None
。如何防止这种情况发生?
(我注意到
如何在 python argparse 中使用子解析器定义全局选项?是类似的,那里的答案建议为顶级和子级解析器使用不同的 dest
变量。我会想避免这种情况...)
action='count'
(从 Python 3.12.3 开始)根本无法实现问题中
ArgumentParser
所需的行为,因为子命名空间最后如何合并到主命名空间中。 具体来说,内部
_CountAction
callable会尝试在当前命名空间中查找
dest
,如果不存在则将其设置为
0
,并将值增加
1
。 然而,内部
_SubparserAction
callable只会天真地合并结果值(不是确切的,但非常相关的 已知问题)。 您可以通过使用以下 argparser 跟踪执行来验证这一点(它设置了额外的默认标志以消除歧义以及示例输入),并单步执行调试器:
import argparse
common = argparse.ArgumentParser(add_help=False)
common.add_argument("-v", action="count")
common.add_argument("-c", default="common")
parser = argparse.ArgumentParser(parents=[common])
parser.add_argument("-p", default="parser")
sub = parser.add_subparsers().add_parser("test", parents=[common])
sub.add_argument("-s", default="sub")
args = ['-v', '-v', 'test', '-v', '-v']
print(f"final: {parser.parse_args(args)}")
除了单步执行调试器之外,在第二个代码块中的 print(namespace)
块之前插入
_CountAction.__call__
和
print(f"{subnamespace} merging into {namespace}")
的链接行上方,将产生以下输出:
for
我多年前在我编写的一个包中解决了这个特定问题(具体来说,this commit,它暗示了这个非常具体的问题),方法是使用通用解析器和
Namespace(v=None, c='common', p='parser')
Namespace(v=1, c='common', p='parser')
Namespace(v=None, c='common', s='sub')
Namespace(v=1, c='common', s='sub')
Namespace(v=2, c='common', s='sub') merging into Namespace(v=2, c='common', p='parser')
final: Namespace(v=2, c='common', p='parser', s='sub')
以及传入的参数。 由于您的最后一个示例已经有一个通用解析器,因此将最后部分更改为:
parse_known_args
应产生以下输出:print(f"common: {common.parse_known_args(args)}")
print(f"final: {parser.parse_args(args)}")
从
common: (Namespace(v=4, c='common'), ['test'])
final: Namespace(v=2, c='common', p='parser', s='sub')
的已知参数中选择您需要的内容(并忽略其余部分),并从 common
中选择剩余部分。