Python Argparse: Group Sub-Parsers
Python argparse has become a staple package in python in part due to its ease of use.
However, I recently came across an issue while using it on InsteonMQTT which makes extensive use of sub-parsers. Other than sorting, there is no mechanism to organize sub-parser objects to make them more readable.
This seems like a known issue going back to at least 2009 with no indication that it will be solved. Luckily, Steven Bethard was nice enough to propose a patch for argparse that I was able to convert to a module extension very easily.
In short, the following is the module extension argparse_ext.py:
#===========================================================================
#
# Extend Argparse to Enable Sub-Parser Groups
#
# Based on this very old issue: https://bugs.python.org/issue9341
#
# Adds the method `add_parser_group()` to the sub-parser class.
# This adds a group heading to the sub-parser list, just like the
# `add_argument_group()` method.
#
# NOTE: As noted on the issue page, this probably won't work with [parents].
# see http://bugs.python.org/issue16807
#
#===========================================================================
# Pylint doesn't like us access protected items like this
#pylint:disable=protected-access,abstract-method
import argparse
class _SubParsersAction(argparse._SubParsersAction):
class _PseudoGroup(argparse.Action):
def __init__(self, container, title):
sup = super(_SubParsersAction._PseudoGroup, self)
sup.__init__(option_strings=[], dest=title)
self.container = container
self._choices_actions = []
def add_parser(self, name, **kwargs):
# add the parser to the main Action, but move the pseudo action
# in the group's own list
parser = self.container.add_parser(name, **kwargs)
choice_action = self.container._choices_actions.pop()
self._choices_actions.append(choice_action)
return parser
def _get_subactions(self):
return self._choices_actions
def add_parser_group(self, title):
# the formatter can handle recursive subgroups
grp = _SubParsersAction._PseudoGroup(self, title)
self._choices_actions.append(grp)
return grp
def add_parser_group(self, title):
#
grp = _SubParsersAction._PseudoGroup(self, title)
self._choices_actions.append(grp)
return grp
class ArgumentParser(argparse.ArgumentParser):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.register('action', 'parsers', _SubParsersAction)
And the following is a simple test file test.py:
import argparse_ext
parser = argparse_ext.ArgumentParser(prog='PROG')
cmd = parser.add_subparsers(dest='cmd')
grp1 = cmd.add_parser_group('group1:')
grp1.add_parser('a', help='a subcommand help', aliases=['a1','a2'])
grp1.add_parser('b', help='b subcommand help')
grp1.add_parser('c', help='c subcommand help')
grp2 = cmd.add_parser_group('group2:')
grp2.add_parser('d', help='d subcommand help')
grp2.add_parser('e', help='e subcommand help', aliases=['e1'])
parser.print_help()
Which produces this nice command line output:
...$ python test.py
usage: PROG [-h] {a,a1,a2,b,c,d,e,e1} ...
positional arguments:
{a,a1,a2,b,c,d,e,e1}
group1:
a (a1, a2) a subcommand help
b b subcommand help
c c subcommand help
group2:
d d subcommand help
e (e1) e subcommand help
optional arguments:
-h, --help show this help message and exit
Note: There is a warning that this code may not work with parents argument of ArgumentParser, but I can live with that.