i have an ArgumentParser
with sub-parsers. Some flags are common for all sub-parsers, and I would like to be able to specify them either before or after the sub-command, or even mix before and after (at the user's discretion).
Something like this:
$ ./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)
So I tried something like this:
#!/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())
Running this program "test.py" gives me:
./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)
cool.
but it also gives me:
$ ./test.py -vvv test
Namespace(v=None)
$ ./test.py -vv test -vv
Namespace(v=2)
less cool :-(
I also tried to specify parent-parsers explicitly:
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())
but the result is the same.
So, I guess that as soon as the test
subparser kicks in, it resets the value if v
to None
.
How do I prevent this?
(I notice that How can I define global options with sub-parsers in python argparse? is similar, and the answer there suggests to use different dest
variables for the top-level and the sub-level parser. i would like to avoid that...)
The desired behavior for action='count'
as per the question is simply not possible using a single ArgumentParser
(as of Python 3.12.3), because of how sub-namespace gets merged into the main namespace at the end. Specifically, the internal _CountAction
callable will find try to find the dest
in the current namespace, set it to 0
if not already exist, and increment the value by 1
. However, the internal _SubparserAction
callable will just naively merge the resulting values in (not an exact but very much related known issue). You may verify this by tracing through the execution using following argparser (which is set up with additional default flags for disambiguation along with example input), and step through the debugger:
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']
# demo
print(f"parser: {parser.parse_args(args)}")
Alternate to stepping through the debugger, insert print(namespace)
above the linked lines for _CountAction.__call__
and print(f"{subnamespace} merging into {namespace}")
before the for
block in the second code block, will produce the following output:
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')
parser: Namespace(v=2, c='common', p='parser', s='sub')
I've worked around this specific issue many years ago in a package I've written (specifically, this commit, which hinted at this very specific woe) by using a common parser and parse_known_args
with the incoming arguments. Since your final example already has a common parser, change the demo portion to be:
print(f"common: {common.parse_known_args(args)}")
print(f"parser: {parser.parse_args(args)}")
Should produce the following output:
common: (Namespace(v=4, c='common'), ['test'])
parser: Namespace(v=2, c='common', p='parser', s='sub')
Pick what you need from the known args of common
(and ignore the remainder), and parse the list of args as normal with parser
and use the rest of it as normal.