Subclassing argparse.ArgumentParser is producing weird behaviour

Viewed 129

I'm subclassing ArgumentParser so that I can DRY up the common code I have across multiple commands in my package. However, I'm running into some weird behaviour which I'm not able to explain.

See this very simplified version of my program and note the two lines with comments at the end:

class Parser(argparse.ArgumentParser):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._add_common_args() # (1)

    def _add_common_args(self):
        self.add_argument('-s', '--stage', required=True, type=str.lower)

    def parse(self):
        return self.parse_args(['--stage', 'alpha', 'list-services'])

parser = Parser(prog='my_prog')
# parser._add_common_args() # (2)

subparsers = parser.add_subparsers(dest='resource')
subparsers.required = True

services_parser = subparsers.add_parser('list-services')

parser.parse()

If I run this, the program returns

usage: my_prog list-services [-h] -s STAGE
my_prog list-services: error: the following arguments are required: -s/--stage

But, if I comment out the line at (1) and uncomment the line at (2), it works as expected.

What's going on here?

1 Answers
In [3]: parser.print_help()
usage: my_prog [-h] -s STAGE {list-services} ...

positional arguments:
  {list-services}

optional arguments:
  -h, --help            show this help message and exit
  -s STAGE, --stage STAGE

Look at the help for services_parser:

In [4]: services_parser.print_help()
usage: my_prog list-services [-h] -s STAGE

optional arguments:
  -h, --help            show this help message and exit
  -s STAGE, --stage STAGE

Both parsers where created using the custom Parser class. When that class adds the '-s' as part of creation, both get that action:

In [5]: type(parser)
Out[5]: __main__.Parser
In [6]: type(services_parser)
Out[6]: __main__.Parser

So you have to provide that argument in both - but the second overwrites the first:

In [7]: parser.parse_args('-s foo list-services -s other'.split())
Out[7]: Namespace(resource='list-services', stage='other')

In general using the same argument (with same dest) in both is a bad idea. The subparser value (even the default) dominates.

Your version (2) only sets '-s' on the main.

If I specify an alternative parser_class, only the main parser gets the '-s':

subparsers = parser.add_subparsers(dest='resource', 
     parser_class=argparse.ArgumentParser)

In [9]: type(services_parser)
Out[9]: argparse.ArgumentParser
In [12]: parser.parse()
Out[12]: Namespace(resource='list-services', stage='alpha')

Alternatively you could make the main parser the regular class, and the subparsers the custom one.

Related