Search code examples
pythonpython-3.xargparsesubparsers

How can I add more subparsers to an argpare parser?


If my code calls a function which returns an ArgumentParser that already has some subparsers defined, how can I add more subparsers?

I'd like to do something this:

parser_with_subparsers = build_parser()
additional_subparsers = parser_with_subparsers.add_subparsers()
extra_subparser = additional_subparsers.add_parser("extra", help="extra help"))

But when I attempt that in the interpreter:

>>> parser_with_subparsers = build_parser()
>>> additional_subparsers = parser_with_subparsers.add_subparsers()
usage: [-h] {original_subparser} ...
: error: cannot have multiple subparser arguments

Note that I do not have access to the internals of the build_parser() function.

The only solution I can think of is using parents, but this has the side effect of grouping the subparsers in a strange way:

>>> child_parser = argparse.ArgumentParser(parents=[build_parser()])
>>> additional_subparsers = child_parser.add_subparsers()
>>> extra_subparser = additional_subparsers.add_parser("extra", help="extra help")
>>> extra_subparser2 = additional_subparsers.add_parser("extra2", help="extra2 help")
>>> child_parser.print_help()
usage: [-h] {original_subparser} ... {extra,extra2} ...

positional arguments:
  {original_subparser}
    original_subparser  original_subparser help
  {extra,extra2}
    extra               extra help
    extra2              extra2 help

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

The reason I want to do this is that I want to add extra 'commands' to an Argument Parser that I have inherited.


Solution

  • What kind of behavior are you expecting from the added subparsers?

    sp = parser.add_subparsers(...)
    

    add_subparsers creates a positional argument of _SubParsersAction class, and makes a note of it in a parser._subparsers attribute. It raises that error if that attribute has already been set. sp is that Action object.

    Logically it doesn't make sense to have more than one 'subparsers' action. When the main parser encounters of a subparser command, it delegates the parsing to that subparser. Apart from some cleanup error checking the main parser does not resume parsing. So it couldn't handle a second subparser command.

    (the terminology for what add_subparsers creates and what add_parser does can be a confusing. One is a positional argument/Action, the other a parser (instance of ArgumentParser).

    It should be possible to add new parsers to an existing subparsers Action (I'd have to experiment to find a clean way of doing this). It also possible to add nest subparsers, that is, define add_subparsers for a subparser.


    That parents approach bypasses thismultiple subparser arguments test, probably because it doesn't set the parser._subparsers attribute the first time (when copying Actions from the parent). So that in effect creates two subparser arguments. That's what the help shows. But my guess is that the parsing will not be meaningful. It will still expect 'original_subparser' and not look for 'extra' or 'extra2'.

    The parents mechanism is not robust. There are straight forward uses of it, but it's easily broken.


    In [2]: parser = argparse.ArgumentParser(prog='main')
    In [3]: a1 = parser.add_argument('--foo')
    In [4]: parser._subparsers    # initial value None
    
    In [5]: sp = parser.add_subparsers(dest='cmd')
    In [6]: parser._subparsers    # new non-None value
    Out[6]: <argparse._ArgumentGroup at 0xaf780f0c>
    

    The sp object, an Action subclass:

    In [8]: sp
    Out[8]: _SubParsersAction(option_strings=[], dest='cmd', nargs='A...', const=None, default=None, type=None, choices=OrderedDict(), help=None, metavar=None)
    In [9]: sp1 = sp.add_parser('cmd1')
    

    sp is an Action (positional argument); sp1 is a parser.

    In [10]: parser.print_help()
    usage: main [-h] [--foo FOO] {cmd1} ...
    
    positional arguments:
      {cmd1}
    
    optional arguments:
      -h, --help  show this help message and exit
      --foo FOO
    

    The _actions attribute is a list of actions. Note the help, and foo

    In [12]: parser._subparsers._actions
    Out[12]: 
    [_HelpAction(option_strings=['-h', '--help'], dest='help',...),
     _StoreAction(option_strings=['--foo'], dest='foo',....),
     _SubParsersAction(option_strings=[], dest='cmd', ...]
    

    So the last item in this list is the sp object

    In [13]: parser._subparsers._actions[-1]
    Out[13]: _SubParsersAction(option_strings=[], dest='cmd', nargs='A...', const=None, default=None, type=None, choices=OrderedDict([('cmd1', ArgumentParser(prog='main cmd1', usage=None, description=None, formatter_class=<class 'argparse.HelpFormatter'>, conflict_handler='error', add_help=True))]), help=None, metavar=None)
    

    So we can added a new subparser with

    In [14]: parser._subparsers._actions[-1].add_parser('extra')
    ...
    In [15]: parser.print_help()
    usage: main [-h] [--foo FOO] {cmd1,extra} ...
    
    positional arguments:
      {cmd1,extra}
    
    optional arguments:
      -h, --help    show this help message and exit
      --foo FOO
    

    So if you want just add extra and extra2 to original_subparser, this should work.

    So as long as we don't try further parser.add_argument(...) grabbing the last _actions item should work. If it isn't the last, we'd have to do a more sophisticate search.

    Judging from my rep change, someone found this earlier exploration of the _subparsers attribute:

    How to obtain argparse subparsers from a parent parser (to inspect defaults)