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.