Python argparse override help from parent

2019-08-19 10:18发布

问题:

I have a CLI I'm building which utilizes subparsers for sub-commands similar to tools like git. Some of my sub-commands share common options so I have a group parser that defines the options, and each sub-command that needs them uses parents=group_parser as one of the arguments. For example:

group_parser = argparse.ArgumentParser()
group_parser.add_argument('-f', '--foo', action='store_true')

command1_parser = subparsers.add_parser('command1', parents=[group_parser])
command2_parser = subparsers.add_parser('command2', parents=[group_parser])

So as you see both command1 and command2 inherit the option --foo. What I'm trying to do is update the helptext for foo separately on command1 and command2. For instance if I run myprog command1 -h, I want it to say a different help message for --foo that when I run myprog command2 -h. The problem is until I do parse_args() there isn't a namespace to update for that argument, so something like this doesn't work:

group_parser = argparse.ArgumentParser()
group_parser.add_argument('-f', '--foo', action='store_true')

command1_parser = subparsers.add_parser('command1', parents=[group_parser])
command1.foo['help'] = "foo help for command1"
command2_parser = subparsers.add_parser('command2', parents=[group_parser])
command2.foo['help'] = "foo help for command2"

Is this possible to somehow add additional parameters to an argument outside of the add_argument() function? The only other solution is to not use the inherited parent and just define foo separately for each sub-command, but if there's a way to update parameters, that would be ideal.

回答1:

There is a way of changing the help (and other Action attributes) after the Action (argument) has been created. But with the parents mechanism there's another problem - Actions are copied by reference. So even if you can change the help for command1, you end up changing it for command2 as well. I've encountered this when trying to change attributes like default.

I'll add an illustration, and may be a link to previous discussions.

In [2]: parent = argparse.ArgumentParser(add_help=False)
In [4]: fooObj = parent.add_argument('--foo',default='foo1', help='foo help')

fooObj is a reference to the Action created by this add_argument.

In [5]: fooObj.default
Out[5]: 'foo1'
In [6]: fooObj.help      # the help parameter        
Out[6]: 'foo help'
In [7]: parent.print_help()
usage: ipython3 [--foo FOO]

optional arguments:
  --foo FOO  foo help

changing the help attribute:

In [8]: fooObj.help = 'new help'
In [9]: parent.print_help()
usage: ipython3 [--foo FOO]

optional arguments:
  --foo FOO  new help

Now make a parser and subparsers

In [10]: parser = argparse.ArgumentParser()
In [11]: sp = parser.add_subparsers()
In [13]: cmd1 = sp.add_parser('cmd1',parents=[parent])
In [14]: cmd2 = sp.add_parser('cmd2',parents=[parent])

In [15]: cmd2.print_help()
usage: ipython3 cmd2 [-h] [--foo FOO]

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

_actions is a list of the arguments defined for a parser:

In [16]: cmd1._actions
Out[16]: 
[_HelpAction(option_strings=['-h', '--help'], dest='help', nargs=0, const=None, default='==SUPPRESS==', type=None, choices=None, help='show this help message and exit', metavar=None),
 _StoreAction(option_strings=['--foo'], dest='foo', nargs=None, const=None, default='foo1', type=None, choices=None, help='new help', metavar=None)]

Comparing ids we can see that the 2d action is the same as fooObj. Same with cmd2.

In [17]: id(cmd1._actions[1])
Out[17]: 2885458060
In [18]: id(fooObj)
Out[18]: 2885458060

Changing the help for cmd1 changes it for cmd2 as well

In [19]: cmd1._actions[1].help = 'cmd1 foo'
In [20]: cmd2.print_help()
usage: ipython3 cmd2 [-h] [--foo FOO]

optional arguments:
  -h, --help  show this help message and exit
  --foo FOO   cmd1 foo
In [21]: fooObj.help
Out[21]: 'cmd1 foo'

Here's a case of trying to provide different defaults for the subparsers:

argparse - Combining parent parser, subparsers and default values

It might be best to use your own utility function(s) to add the common arguments to the subparsers. That way each subparser can have its own copy of the Action objects, rather than sharing them. I think the parents mechanism is nicer in theory than in practice.