Search code examples
pythonargparse

Argparse subparser help displayed before options


With argparse, currently my main.py --help looks like this

usage: main.py [--options] <command>

options:
  -h, --help  show this help message and exit
  --debug     Logs extra information for debugging
  --version   show program's version number and exit
  --license   reads the GPLv3

commands:

    install   install packages
    remove    remove packages
    update    update package list and upgrade the system

is there an easy way to make the subparser commands display before the global options?

my configuration is a bit long to post at the moment but I can if context is needed. I don't have anything crazy going on, this is the important stuff.

formatter = lambda prog: argparse.RawDescriptionHelpFormatter(prog,
                                                max_help_position=64)
bin_name = Path(argv[0]).name
version = __version__
parser = nalaParser(    formatter_class=formatter,
                        usage=f'{bin_name} [--options] <command>', 
                        )

subparsers = parser.add_subparsers(title='commands', metavar='', dest='command')
parser._optionals.title = "options"

install_parser = subparsers.add_parser('install', help='install packages')
remove_parser = subparsers.add_parser('remove', help='remove packages')
update_parser = subparsers.add_parser('update', help='update package list and upgrade the system')

parser.add_argument('--debug', action='store_true', help='Logs extra information for debugging')
parser.add_argument('--version', action='version', version=f'{bin_name} {version}')
parser.add_argument('--license', action=GPLv3)

Since I was already subclassing I just added this method below to nalaParser

def format_help(self):
    formatter = self._get_formatter()

    # usage
    formatter.add_usage(self.usage, self._actions,
                        self._mutually_exclusive_groups)

    # description
    formatter.add_text(self.description)

    index = -1
    # Put options last in the group
    for action_group in self._action_groups[:]:
        index = + 1
        if action_group.title == 'options':
            self._action_groups.append(self._action_groups.pop(index))

    # positionals, optionals and user-defined groups
    for action_group in self._action_groups:
        formatter.start_section(action_group.title)
        formatter.add_text(action_group.description)
        formatter.add_arguments(action_group._group_actions)
        formatter.end_section()


    # epilog
    formatter.add_text(self.epilog)

    # determine help from format above
    return formatter.format_help()

Solution

  • parser.format_help passes the action_groups to the formatter in the order that they were created.

    In [24]: parser._action_groups
    Out[24]: 
    [<argparse._ArgumentGroup at 0x7f8167ac02e0>,
     <argparse._ArgumentGroup at 0x7f8167ac0850>,
     <argparse._ArgumentGroup at 0x7f8167ac0490>]
    In [25]: [a.title for a in parser._action_groups]
    Out[25]: ['positional arguments', 'options', 'commands']
    

    The default groups are positional and options. add_subparsers puts its Action in the positional group, except when given a title. In that case it makes a new group.

    You could tweak the format_help to reorder the groups that it passes to the formatter.

    Or it might work to change the 'positional arguments' title as you did with the optionals (instead of giving subparsers the title parameter).

    Anyways, the key to the behavior is in these two methods.