Argparse suggests nonsensical order in help text usage line - python

This program:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('files', metavar='INPUT', nargs='*',
help='File(s) containing words to include. If none given, stdin will be used.')
parser.add_argument('-x', '--exclude', nargs='*',
help='File(s) containing words to exclude.')
args = parser.parse_args()
print args.files
print args.exclude
produces this output when run in Python 2.7.9:
$ python prog.py --help
usage: prog.py [-h] [-x [EXCLUDE [EXCLUDE ...]]] [INPUT [INPUT ...]]
positional arguments:
INPUT File(s) containing words to include. If
none given, stdin will be used.
optional arguments:
-h, --help show this help message and exit
-x [EXCLUDE [EXCLUDE ...]], --exclude [EXCLUDE [EXCLUDE ...]]
File(s) containing words to exclude.
However, that "help" output instructs the user to use a nonsensical ordering for the arguments. It is nonsensical because if the -x option is used, then no INPUT arguments will be detected.
Argparse ought instead to advise the user to use this ordering:
usage: prog.py [-h] [INPUT [INPUT ...]] [-x [EXCLUDE [EXCLUDE ...]]]
Two questions:
Is this a bug in argparse? (I think it is.)
Regardless of whether it is a bug, how can I fix it so that $ python prog.py --help will output the help text I desire (see above), preferably in as DRY a way as possible?

When generating the usage line, flagged arguments are placed first, positional after. That fits with common commandline usage. It makes no effort to evaluate whether that is the best choice or not.
The simplest way around this is to provide a custom usage parameter.
(there have be SO questions about changing the order of arguments in the usage line. The solution requires customization of the HelpFormatter class. Change argparse usage message argument order
)
As you note, when the * positional is placed after the * optional, all arguments are assigned to the optional. You could use a '--' to separate the two lists of arguments.
There is a bug/issue with patch that should improve handling when the positional takes a known number of arguments (e.g. the default one). It does so by noting that the positional requires an argument, so it reserves one for it. But in the * * case, the positional is satisfied with none, so it has no way of knowing how to split the arguments between the 2.
I like the idea of turning that postional into a flagged argument. That should reduce the ambiguity inherent in lots of * arguments.

Add '-f', '--files' to the input option:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-f', '--files', metavar='INPUT', nargs='*', required=True,
help='File(s) containing words to include. If none given, stdin will be used.')
parser.add_argument('-x', '--exclude', nargs='*',
help='File(s) containing words to exclude.')
args = parser.parse_args()
print args.files
shows:
usage: argparse_test.py [-h] [-f [INPUT [INPUT ...]]]
[-x [EXCLUDE [EXCLUDE ...]]]
optional arguments:
-h, --help show this help message and exit
-f [INPUT [INPUT ...]], --files [INPUT [INPUT ...]]
File(s) containing words to include. If none given,
stdin will be used.
-x [EXCLUDE [EXCLUDE ...]], --exclude [EXCLUDE [EXCLUDE ...]]
File(s) containing words to exclude.
print args.exclude
You can make 'files' required. From the docs:
In general, the argparse module assumes that flags like -f and --bar indicate optional arguments, which can always be omitted at the command line. To make an option required, True can be specified for the required= keyword argument to add_argument():

The simplest way is to add usage="..." to argparse.ArgumentParser().
By viewing the source of argparse, I found a way to resort arguments which might be a little bit dirty:
class MyHelpFormatter(argparse.HelpFormatter):
def _format_actions_usage(self, actions, groups):
actions.sort(key=lambda a: bool(a.option_strings and a.nargs != 0))
return super(MyHelpFormatter, self)._format_actions_usage(actions, groups)
parser = argparse.ArgumentParser(formatter_class = MyHelpFormatter)

Related

Multiple arguments with the same type

I'm using argparse to generate CLI. There are two positional arguments which are all of type str and can occur several time (i.e., nargs='+').
What my current code looks like is :
parser.add_argument('-a', type=str, nargs='+', required=True,
help='argument a')
parser.add_argument('-b', type=str, nargs='+', required=True,
help='argument b')
However, this is not a good solution as both of a and b are "optional arguments" in the help document. I want to make a and b both positional.
Is there any way to do so in argparse?
Such as:
python main.py a=["a_1", "a_2"] b=["b_1", "b_2"]
help with your initial definition is:
usage: stack52580176.py [-h] -a A [A ...] -b B [B ...]
optional arguments:
-h, --help show this help message and exit
-a A [A ...] argument a
-b B [B ...] argument b
The usage line shows that -a requires at least one argument; same for -b.
Yes, the 2 arguments are in the optional arguments group. The only default alternative is positional arguments. There isn't a 'required' group, or 'required but flagged' group.
But replacing input like
python main.py -a a_1 a_2 -b b_1 b_2
with:
python main.py a=["a_1", "a_2"] b=["b_1", "b_2"]
will be awkward for both your users and yourself. It'll need some extra quotes to keep the shell from splitting on all spaces. And you'll have to parse the string 'a=["a_1", "a_2"]' yourself.
But if it's just the group name that bothers you, we can change that with an argument_group
import argparse
parser = argparse.ArgumentParser()
g = parser.add_argument_group('required arguments:')
g.add_argument('-a', type=str, nargs='+', required=True,
help='argument a')
g.add_argument('-b', type=str, nargs='+', required=True,
help='argument b')
parser.print_help()
which produces:
usage: stack52580176.py [-h] -a A [A ...] -b B [B ...]
optional arguments:
-h, --help show this help message and exit
required arguments::
-a A [A ...] argument a
-b B [B ...] argument b
If the arguments hadn't been marked as required, usage would have had extra []:
usage: stack52580176.py [-h] [-a A [A ...]] [-b B [B ...]]
The possibility of specifying required (and '?' nargs) blurs the conventional distinction between optionals and positionals. We can have required optionals, and optional positionals.

argparse: extraneous arguments for argument with no stored value?

Having a bit of problem with argparse in Python...
import argparse
parser = argparse.ArgumentParser()
parser.add_argument ("-o", "--optional", help="this is an optional argument")
args = parser.parse_args()
print ( args.optional )
Calling test.py -h will output...
usage: test.py [-h] [-o OPTIONAL]
optional arguments:
-h, --help show this help message and exit
-o OPTIONAL, --optional OPTIONAL
this is an optional argument
Is there any way I can get rid of the extra OPTIONALs in the help menu? I know I could do this with parser.add_argument ("-o", "--optional", help="this is an optional argument", action=store_true), except I can't because I need to call args.optional later on.
Again, this isn't so much about the functionality of the program as the aesthetics because test.py -o hello would print hello.
Usually an option with no arguments has an action, which will suppress that metavar:
parser.add_argument ("-o", "--optional", action='store_true')
Otherwise, you could amend the argument like this:
parser.add_argument ("-o", "--optional", metavar='', help="the help text")
First is this about the parsing of this argument or the display in the help?
parser.add_argument ("-o", "--optional", help="this is an optional argument")
has the default store_true action, and thus takes one argument. As indicated in the usage:
usage: test.py [-h] [-o OPTIONAL]
where 'OPTIONAL' is a standin for the string that you will include after the -o or --optional. And args.optional will have the value of that string.
action='store_true turns this argument into a boolean, False if not given, True if -o is provided. It takes to no added value.
-o OPTIONAL, --optional OPTIONAL
is the normal way an Action like this is displayed in the help. Again OPTIONAL is the place marker for the string that follows -o or --optional. A metavar parameter can be used to customize that place marker. It can be as short as "".
Some people don't like that repeated pattern, preferring something like
-o , --optional OPTIONAL
That has been discussed in previous questions. It requires a change to the HelpFormatter class (ie. a subclassing).
python argparse help message, disable metavar for short options?
Another thing is to simplify the definition
parser.add_argument ("-o", dest="optional", help="this is an optional argument")

Reorder Python argparse argument groups

I'm using argparse and I have a custom argument group required arguments. Is there any way to change the order of the argument groups in the help message? I think it is more logical to have the required arguments before optional arguments, but haven't found any documentation or questions to help.
For example, changing this:
usage: foo.py [-h] -i INPUT [-o OUTPUT]
Foo
optional arguments:
-h, --help show this help message and exit
-o OUTPUT, --output OUTPUT
Output file name
required arguments:
-i INPUT, --input INPUT
Input file name
to this:
usage: foo.py [-h] -i INPUT [-o OUTPUT]
Foo
required arguments:
-i INPUT, --input INPUT
Input file name
optional arguments:
-h, --help show this help message and exit
-o OUTPUT, --output OUTPUT
Output file name
(example taken from this question)
You might consider adding an explicit optional arguments group:
import argparse
parser = argparse.ArgumentParser(description='Foo', add_help=False)
required = parser.add_argument_group('required arguments')
required.add_argument('-i', '--input', help='Input file name', required=True)
optional = parser.add_argument_group('optional arguments')
optional.add_argument("-h", "--help", action="help", help="show this help message and exit")
optional.add_argument('-o', '--output', help='Output file name', default='stdout')
parser.parse_args(['-h'])
You can move the help action to your optional group as
described here:
Move "help" to a different Argument Group in python argparse
As you can see, the code produces the required output:
usage: code.py -i INPUT [-h] [-o OUTPUT]
Foo
required arguments:
-i INPUT, --input INPUT
Input file name
optional arguments:
-h, --help show this help message and exit
-o OUTPUT, --output OUTPUT
Output file name
This is admittedly a hack, and is reliant on the changeable internal implementation, but after adding the arguments, you can simply do:
parser._action_groups.reverse()
This will effectively make the required arguments group display above the optional arguments group. Note that this answer is only meant to be descriptive, not prescriptive.
Credit: answer by hpaulj
The parser starts out with 2 argument groups, the usual positional and optionals. The -h help is added to optionals. When you do add_argument_group, a group is created (and returned to you). It is also appended to the parser._action_groups list.
When you ask for help (-h) parser.format_help() is called (you can do that as well in testing). Look for that method in argparse.py. That sets up the help message, and one step is:
# 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()
So if we reorder the items in the parser._action_groups list, we will reorder the groups in the display. Since this is the only use of _action_groups it should be safe and easy. But some people aren't allowed to peak under the covers (look or change ._ attributes).
The proposed solution(s) is to make your own groups in the order you want to see them, and make sure that the default groups are empty (the add_help=False parameter). That's the only way to do this if you stick with the public API.
Demo:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('foo')
g1 = parser.add_argument_group('REQUIRED')
g1.add_argument('--bar', required=True)
g1.add_argument('baz', nargs=2)
print(parser._action_groups)
print([group.title for group in parser._action_groups])
print(parser.format_help())
parser._action_groups.reverse() # easy inplace change
parser.print_help()
Run result:
1504:~/mypy$ python stack39047075.py
_actions_group list and titles:
[<argparse._ArgumentGroup object at 0xb7247fac>,
<argparse._ArgumentGroup object at 0xb7247f6c>,
<argparse._ArgumentGroup object at 0xb721de0c>]
['positional arguments', 'optional arguments', 'REQUIRED']
default help:
usage: stack39047075.py [-h] --bar BAR foo baz baz
positional arguments:
foo
optional arguments:
-h, --help show this help message and exit
REQUIRED:
--bar BAR
baz
after reverse:
usage: stack39047075.py [-h] --bar BAR foo baz baz
REQUIRED:
--bar BAR
baz
optional arguments:
-h, --help show this help message and exit
positional arguments:
foo
1504:~/mypy$
Another way to implement this is to define a ArgumentParser subclass with a new format_help method. In that method reorder the list used in that for action_group... loop.

Argparse - do not catch positional arguments with `nargs`.

I am trying to write a function wo which you can parse a variable amount of arguments via argparse - I know I can do this via nargs="+". Sadly, the way argparse help works (and the way people generally write arguments in the CLI) puts the positional arguments last. This leads to my positional argument being caught as part of the optional arguments.
#!/usr/bin/python
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("positional", help="my positional arg", type=int)
parser.add_argument("-o", "--optional", help="my optional arg", nargs='+', type=float)
args = parser.parse_args()
print args.positional, args.optional
running this as ./test.py -h shows the following usage instruction:
usage: test.py [-h] [-o OPTIONAL [OPTIONAL ...]] positional
but if I run ./test.py -o 0.21 0.11 0.33 0.13 100 gives me
test.py: error: too few arguments
to get a correct parsing of args, I have to run ./test.py 100 -o 0.21 0.11 0.33 0.13
So how do I:
make argparse reformat the usage output so that it is less misleading, OR, even better:
tell argparse to not catch the last element for the optional argument -o if it is the last in the list
?
There is a bug report on this: http://bugs.python.org/issue9338
argparse optionals with nargs='?', '*' or '+' can't be followed by positionals
A simple (user) fix is to use -- to separate postionals from optionals:
./test.py -o 0.21 0.11 0.33 0.13 -- 100
I wrote a patch that reserves some of the arguments for use by the positional. But it isn't a trivial one.
As for changing the usage line - the simplest thing is to write your own, e.g.:
usage: test.py [-h] positional [-o OPTIONAL [OPTIONAL ...]]
usage: test.py [-h] [-o OPTIONAL [OPTIONAL ...]] -- positional
I wouldn't recommend adding logic to the usage formatter to make this sort of change. I think it would get too complex.
Another quick fix is to turn this positional into an (required) optional. It gives the user complete freedom regarding their order, and might reduce confusion. If you don't want to confusion of a 'required optional' just give it a logical default.
usage: test.py [-h] [-o OPTIONAL [OPTIONAL ...]] -p POSITIONAL
usage: test.py [-h] [-o OPTIONAL [OPTIONAL ...]] [-p POS_WITH_DEFAULT]
One easy change to the Help_Formatter is to simply list the arguments in the order that they are defined. The normal way of modifying formatter behavior is to subclass it, and change one or two methods. Most of these methods are 'private' (_ prefix), so you do so with the realization that future code might change (slowly).
In this method, actions is the list of arguments, in the order in which they were defined. The default behavior is to split 'optionals' from 'positionals', and reassemble the list with positionals at the end. There's additional code that handles long lines that need wrapping. Normally it puts positionals on a separate line. I've omitted that.
class Formatter(argparse.HelpFormatter):
# use defined argument order to display usage
def _format_usage(self, usage, actions, groups, prefix):
if prefix is None:
prefix = 'usage: '
# if usage is specified, use that
if usage is not None:
usage = usage % dict(prog=self._prog)
# if no optionals or positionals are available, usage is just prog
elif usage is None and not actions:
usage = '%(prog)s' % dict(prog=self._prog)
elif usage is None:
prog = '%(prog)s' % dict(prog=self._prog)
# build full usage string
action_usage = self._format_actions_usage(actions, groups) # NEW
usage = ' '.join([s for s in [prog, action_usage] if s])
# omit the long line wrapping code
# prefix with 'usage:'
return '%s%s\n\n' % (prefix, usage)
parser = argparse.ArgumentParser(formatter_class=Formatter)
Which produces a usage line like:
usage: stack26985650.py [-h] positional [-o OPTIONAL [OPTIONAL ...]]
Instead of using nargs="+", consider using action="append". This requires passing -o in front of each number, but it will not consume arguments unless you actually want it to.

Argparse argument generated help, 'metavar' with choices

When using an argument (optional and positional both have this problem) with the keyword choices, the generated help output shows those choices.
If that same argument also includes a metavar keyword, the list of choices is omitted from the generated output.
What I had in mind, was to show the metavar in the usage line, but actually show the available choices when the 'autohelp' lists positional/optional argument details.
Any simple fixes/workarounds?
I have already started an argparse wrapper for custom help functionality. Perhaps this should be another feature on my TODO list.
You can add the choices to the help text.
parser=argparse.ArgumentParser()
parser.add_argument('-f',metavar="TEST",choices=('a','b','c'),
help='choices, {%(choices)s}')
print parser.format_help()
produces:
usage: stack20328931.py [-h] [-f TEST]
optional arguments:
-h, --help show this help message and exit
-f TEST choices, {a, b, c}

Categories