argparse: extraneous arguments for argument with no stored value? - python

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")

Related

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 suggests nonsensical order in help text usage line

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)

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.

Override the positional and optional arguments with another argument in command line (argparse python module)

I am using argparser to parse the command line arguments.
Now, I have something like
./script.py 1112323 0 --salary 100000 -- age 34
Here first two are positional arguments and rest are optional.
Now, I want to have a feature such that when the user gives a filename as input in command line, then it should override these above arguments and take the arguments from header of the file. I meam when user gives sth like
id|sequence|age|name|........... (header of the file with first two cols as positional arguments and rest positional)
On giving this in command line:
./script.py -f filename
it should not complain of above positional arguments.
Is this feasible over my current implementation?
You will most likely need to implement this check yourself. Make both arguments (positional and -f) optional (required=False and nargs="*") and then implement your custom check and use the error method of ArgumentParser. To make it easier for user mention the correct usage in help string.
Something like this:
parser = ArgumentParser()
parser.add_argument("positional", nargs="*", help="If you don't provide positional arguments you need use -f")
parser.add_argument("-f", "--file", required=False, help="...")
args = parser.parse_args()
if not args.file and not args.positional:
parser.error('You must use either -f or positional argument')

Argparse positional arguments with prefix

My python3 script works with the input and the output file which is specified on the command line.
The usage should looks like this
xxxx.py [-h] --input=file --output=file
In code I am using
parser.add_argument("input", help='Input file');
parser.add_argument("output", help='Output file');
but the arguments are without the necessary prefix. Is there a way to specify the prefix for each argument?
Simply include the double-dash:
parser.add_argument("--input", help='Input file');
parser.add_argument("--output", help='Output file');
Arguments are either positional or optional; arguments starting with -- are always optional. You cannot create positional arguments with a -- prefix and you really should not. The -- prefix is a user interface convention you really do not want to break.

Categories