Reputation: 76608
While using argparse with positionals, and default
provided, nargs='*'
works as expected by setting the positional to the default if no positionals are provided. But nargs='+'
does not do this. The default provided is a non-empty list, and it does get correctly shown in the help string in both cases, but the default
is only used when using nargs='*'
.
Is my expectation wrong or is this a bug/inconsistency in argparse?
Program:
import argparse
for narg in '*+':
parser = argparse.ArgumentParser(description=f'test nargs {narg}')
parser.add_argument(
'dir',
nargs=narg,
default=['dir_a', 'dir_b'],
help='provide directories as arguments (default: %(default)s)',
)
try:
args = parser.parse_args(['-h'])
except SystemExit:
pass
args = parser.parse_args([])
print(f'>>>>> args {args}\n')
Output:
usage: testnargs.py [-h] [dir ...]
test nargs *
positional arguments:
dir provide directories as arguments (default: ['dir_a', 'dir_b'])
options:
-h, --help show this help message and exit
>>>>> args Namespace(dir=['dir_a', 'dir_b'])
usage: testnargs.py [-h] dir [dir ...]
test nargs +
positional arguments:
dir provide directories as arguments (default: ['dir_a', 'dir_b'])
options:
-h, --help show this help message and exit
usage: testnargs.py [-h] dir [dir ...]
testnargs.py: error: the following arguments are required: dir
The real-world program this problem arose in, reads in the defaults from an optional config file. So I don't know if there is always going to be a default, otherwise I could of course change the '+'
into '*'
permanently. For now I did change to '*'
, and check down the line, but it is hassle.
This was run with Python 3.10.5 on MacOS.
Upvotes: 1
Views: 777
Reputation: 4519
I agree that the default shouldn't be shown in the help text if it's not going to be used and in my opinion you should report this (and only this) as a bug. (←not true, see comments)
But regarding the behavior itself, I don't think a default for nargs='+'
makes sense as the whole point that differentiates it from nargs='*'
is that it requires the user to provide at least one argument. If you have a default, you should just use *
, and if you don't know if you have a default in advance as in your case, you could either first check if you have one and then set nargs
depending on that or do what you're doing now by using *
and applying the default afterwards.
Sorry that this isn't very helpful but I see no way around what you're already doing (or variants thereof).
Upvotes: 1
Reputation: 76608
A comment block in argparse.py indicates:
assuming that actions that use the default value don't really count as "present"
So contrary to my assumption that the "bug" is in argparse not accepting the default for nargs='+'
the error is actually in the help formatter as @hpaulj indicated.
Since what I expected is essentially
assuming that actions that have a default are always counted as "present"
I'll have to work around this. Since I generate the argparse code (using cligen) I can relatively easily build in some check that the argument to nargs
changes from '+' to '*' if a default is provided. That would be a short-term solution, it is probably better to generate something simpler than argparse code, that will not generate these kind of surprises.
Upvotes: 1
Reputation: 231375
I'm not as involved with the argparse
bug/issues as I used to be, but I don't consider this to be a bug.
Your help line is simply expanded as follows:
In [78]: help='provide directories as arguments (default: %(default)s'
In [79]: help%{'default': ['a','b']}
Out[79]: "provide directories as arguments (default: ['a', 'b']"
The relevant code methods from the source code are:
def _get_help_string(self, action):
return action.help
def _expand_help(self, action):
params = dict(vars(action), prog=self._prog)
for name in list(params):
if params[name] is SUPPRESS:
del params[name]
for name in list(params):
if hasattr(params[name], '__name__'):
params[name] = params[name].__name__
if params.get('choices') is not None:
choices_str = ', '.join([str(c) for c in params['choices']])
params['choices'] = choices_str
return self._get_help_string(action) % params
help
, default
etc are attributes of the argument's Action
object. It just does a dict
format. If the help has '{%default}' string, it is displayed.
The formatter variant that automatically adds {%default}
, does test whether the Action
is positional and whether the nargs
allows for a meaningful default
.
class ArgumentDefaultsHelpFormatter(HelpFormatter):
"""Help message formatter which adds default values to argument help.
Only the name of this class is considered a public API. All the methods
provided by the class are considered an implementation detail.
"""
def _get_help_string(self, action):
help = action.help
if '%(default)' not in action.help:
if action.default is not SUPPRESS:
defaulting_nargs = [OPTIONAL, ZERO_OR_MORE]
if action.option_strings or action.nargs in defaulting_nargs:
help += ' (default: %(default)s)'
return help
Adding something to the help
is easy. Removing it in any reliable way is not (though you are welcome to write your own _get_help_string
method to do so). The default formatter formats the help as you specify.
Upvotes: 2