How to obtain argparse subparsers from a parent pa

2019-02-20 15:04发布

Suppose that I create a parser with a default value for an argument, and then give it a subparser with a further default value for an argument.

In [1]: parser = argparse.ArgumentParser(description='test')

In [2]: parser.add_argument("--test", dest="test", default="hello")
Out[2]: _StoreAction(option_strings=['--test'], dest='test', nargs=None, const=None, default='hello', type=None, choices=None, help=None, metavar=None)

In [3]: parser.get_default("test")
Out[3]: 'hello'

In [4]: subparsers = parser.add_subparsers(dest="command")

In [5]: parser_other = subparsers.add_parser("other")

In [6]: parser_other.add_argument("--other-test", dest="other_test", default="world")
Out[6]: _StoreAction(option_strings=['--other-test'], dest='other_test', nargs=None, const=None, default='world', type=None, choices=None, help=None, metavar=None)

In [7]: parser_other.get_default("other_test")
Out[7]: 'world'

This is all fine. But suppose that I have a function which creates and returns the parent parser parser from above, but with no direct access to the subparser.

How can I still print out defaults for the subparser arguments? Or get a handle to each subparser separately?

In [8]: parser._subparsers._defaults
Out[8]: {}

In [9]: parser._subparsers.get_default("other_test")  # is None

There doesn't appear to be any more attributes or methods from parser._subparsers or from parser that could display defaults.

The overall problem is: how to programmatically access subparser defaults when you only have a handle to the parent parser?

2条回答
▲ chillily
2楼-- · 2019-02-20 15:25

You got it right. But maybe I can explain a few details.

a = parser.add_argument(...)

add_argument creates an Action object (or actually a subclass depending on the action parameter). You can save a pointer to that object in your own environment. But that Action is also collected in the parse._actions list. That's how the parser keeps tracks of its arguments.

Reading _actions should always be safe. Modifying it risks breaking breaking the parser. argument_groups have access to the list.

subparsers = parser.add_subparsers(dest="command")

is a specialized version of add_argument, creating and returning a argparse._SubParsersAction object. subparsers is that object. And as noted from the earlier answer, you can find it in the _actions list by searching for the correct subclass. (To the main parser, subparsers is just another positional argument.)

subparsers maintains its own specialized dictionary of parsers, accessible as its choices attribute. The main parser does not have any record of those sub parsers.

parser_other = subparsers.add_parser("other")

creates a parser, puts it in that choices map, and returns a reference for your own use (with add_argument etc). Each sub parser has its own _actions list. (and its own _defaults).

Look at the code for the get_defaults method:

def get_default(self, dest):
    for action in self._actions:
        if action.dest == dest and action.default is not None:
            return action.default
    return self._defaults.get(dest, None)

It uses the _actions attribute. And looks at the action.default attribute of the Action.

self._defaults is the dictionary updated by the parser.set_defaults method. That method also copies its parameters to the relevant Action objects. get_defaults checks that in case the dest is one of those defaults that isn't tied to a particular Action. https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.set_defaults

I haven't used the parser._subparsers attribute much. Looking at the parser.add_subparsers method I see it is actually an argument_group. Argument_groups are primarily a help tool, used to group help lines. The relationship between a parser object and its argument_groups is a little tricky, and probably not something you want to use.


Here's an example, with more (too much) detail:

In [22]: parser = argparse.ArgumentParser()
In [23]: sp = parser.add_subparsers(title='subparsers', dest='cmd')
In [24]: sp1 = sp.add_parser('cmd1')
In [25]: sp2 = sp.add_parser('cmd2')
In [26]: parser.print_help()
usage: ipython3 [-h] {cmd1,cmd2} ...

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

subparsers:
  {cmd1,cmd2}

In [28]: [a.dest for a in parser._actions]
Out[28]: ['help', 'cmd']

In [29]: parser._action_groups
Out[29]: 
[<argparse._ArgumentGroup at 0xaf86bf2c>,
 <argparse._ArgumentGroup at 0xaf86bdcc>,
 <argparse._ArgumentGroup at 0xac99fa6c>]
In [30]: [g.title for g in parser._action_groups]
Out[30]: ['positional arguments', 'optional arguments', 'subparsers']

In [31]: parser._subparsers
Out[31]: <argparse._ArgumentGroup at 0xac99fa6c>

The _defaults of _subparsers is actually the same dictionary as parser._defaults

In [32]: parser.set_defaults(extra='foobar')
In [33]: parser._defaults
Out[33]: {'extra': 'foobar'}
In [34]: parser._subparsers._defaults
Out[34]: {'extra': 'foobar'}

parser._subparsers._actions is also identical to parser._actions. But the group does maintain its own list actions (used in the help display).

In [35]: parser._subparsers._group_actions
Out[35]: [_SubParsersAction(option_strings=[], dest='cmd', nargs='A...', const=None, 
    default=None, type=None, choices=OrderedDict([...]), help=None, metavar=None)]

So you could use parser._subparsers._group_actions[0] to find the subparsers action object instead of searching the parsers._actions list.

In [37]: parser._subparsers._group_actions[0].choices
Out[37]: 
OrderedDict([('cmd1',
              ArgumentParser(prog='ipython3 cmd1', usage=None, description=None,...)),
             ('cmd2',
              ArgumentParser(prog='ipython3 cmd2', usage=None, description=None,...))])

On second thought, parser._subparsers._group_actions might not be so useful. If you don't give it a special title, then it is identical to parser._positionals, the argument group of all positional arguments. So you'd still need to verify the _SubParsersAction class.

查看更多
劳资没心,怎么记你
3楼-- · 2019-02-20 15:33

Based on this answer it looks like it can be done as follows:

subparsers = [
    subparser 
    for action in parser._actions 
    if isinstance(action, argparse._SubParsersAction) 
    for _, subparser in action.choices.items()
]

then

subparsers[0].get_default("other_test")

prints "world" as expected.

查看更多
登录 后发表回答