1

Setting the parents argument with a parser will allow for sharing common arguments between parsers (e.g. parents and sub-commands). But, applying a base parser to both the parent and sub-command appears to overwrite the value from the parent parser with the value from the sub-command parser when using an argument that has specified the value attribute to with a dest keyword, whether or not the invocation has specified the argument in the sub-command.

How can I use argparse module to merge the options in the parent and the sub-command (i.e. store the value if either parser contains the option, use the default if neither parser specifies the option, and it doesn't matter how to handle if both parsers specify the option)?

sample.py:

from argparse import ArgumentParser
parser = ArgumentParser(add_help=False) # The "base"
parser.add_argument('-v', '--verbose', dest='verbose', action='store_true')
parser.add_argument('-d', '--dir', dest='dir', default=None)

parser_main = ArgumentParser(parents=[parser])
subparsers = parser_main.add_subparsers(dest='command')
subparsers.add_parser('cmd1', parents=[parser])
args = parser_main.parse_args()

print(str(args))

Then, in the shell:

> sample.py -v -d abc
Namespace(command=None, dir='abc', verbose=True)
> sample.py -v cmd1 -d abc
Namespace(command='cmd1', dir='abc', verbose=False)
> sample.py -d abc cmd1 -v
Namespace(command='cmd1', dir=None, verbose=True)
> sample.py cmd1 -v -d abc
Namespace(command='cmd1', dir='abc', verbose=True)
palswim
  • 11,856
  • 6
  • 53
  • 77
  • Because of how the subparser `namespace` is used to update the parent namespace, its values, including any defaults, clobber corresponding parent attributes. (I could point you to the patch that implemented this some years ago.) In any case, parent `dest` should be distinct if you don't want them to be overwritten. – hpaulj Jul 15 '20 at 00:45
  • `argparse._SubParsersAction.__call__` where `for key, value in vars(subnamespace).items(): setattr(namespace, key, value)` does the update. – hpaulj Jul 15 '20 at 00:48

2 Answers2

2

Using SUPPRESS for the subparser default keeps it from overwriting the parent parser value. A SUPPRESS default is not inserted into the namespace at the start of parsing. A value is written only if the user used that argument.

import argparse    
parser = argparse.ArgumentParser()
parser.add_argument('-f', '--foo', default='foobar')
parser.add_argument('-v', '--verbose', action='store_const', default=False, const=True)

sp = parser.add_subparsers(dest='cmd')
sp1 = sp.add_parser('cmd1')
sp1.add_argument('-f', '--foo', default=argparse.SUPPRESS)
sp1.add_argument('-v', '--verbose', action='store_const', default=argparse.SUPPRESS, const=True)

args = parser.parse_args()
print(args)

sample runs:

1833:~/mypy$ python3 stack62904585.py
Namespace(cmd=None, foo='foobar', verbose=False)
1834:~/mypy$ python3 stack62904585.py --foo FOO -v
Namespace(cmd=None, foo='FOO', verbose=True)
1834:~/mypy$ python3 stack62904585.py cmd1 
Namespace(cmd='cmd1', foo='foobar', verbose=False)
1834:~/mypy$ python3 stack62904585.py -v cmd1 -f bar
Namespace(cmd='cmd1', foo='bar', verbose=True)

The patch that last changed this behavior (2014)

https://bugs.python.org/issue9351 argparse set_defaults on subcommands should override top level set_defaults

also https://bugs.python.org/issue27859

hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • Definitely a better solution than [trying to merge the configuration variables later](https://stackoverflow.com/a/62904586/393280). – palswim Jul 15 '20 at 16:44
0

You can store the value in different attributes by specifying different names with the dest keyword:

from argparse import ArgumentParser
parser = ArgumentParser(add_help=False) # The "base"
parser.add_argument('-v', '--verbose', dest='verbose', action='store_true')
parser.add_argument('-d', '--dir', dest='dir', default=None)

parser_main = ArgumentParser()
parser_main.add_argument('-v', '--verbose', dest='g_verbose', action='store_true')
parser_main.add_argument('-d', '--dir', dest='g_dir', default=None)
subparsers = parser_main.add_subparsers(dest='command')
subparsers.add_parser('cmd1', parents=[parser])
args = parser_main.parse_args()

verbose = args.verbose or args.g_verbose if hasattr(args, 'verbose') else args.g_verbose
d = (args.g_dir if args.dir is None else args.dir) if hasattr(args, 'dir') else args.g_dir
palswim
  • 11,856
  • 6
  • 53
  • 77
  • I really hope this is not the solution. I only posted this answer to pre-empt anyone else who might think this is a reasonable workaround. – palswim Jul 14 '20 at 21:58