Multiple arguments with the same type - python

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.

Related

How to set different options based on combination of positional arguments in Python with argparse?

I'm trying to learn how to use argparse. I'm writing a program with two positional arguments let's say ARG1 and ARG2 and I want to add some options based on the combination of those two positional arguments.
The interface would look like this:
D
C
B B
A A
^ ^
| |
app.py <ARG1> <ARG2> --opt1 --opt2 --opt3 --opt4
The first positional argument ARG1 would have 4 different options and the second positional argument ARG2 would have two different options.
Based on the combination of those two positional arguments I want to specify different options.
For example, when the user enters app.py A B I want only --opt1 --opt2 as required. Some combinations might not use some options at all.
I think subparsers is the way to go but I didn't manage to get it to work.
How can I do that?
Since argparse is designed to handle optionals in any order, it is awkward to force interactions. Subparsers give some control, but can only have one (unless you define subparsers of subparsers).
I think the simplest is to define 2 positionals with choices, and all the optionals, and do any "required" checking after parsing:
In [20]: parser = argparse.ArgumentParser()
In [21]: parser.add_argument('arg1', choices=['A','B','C','D']);
In [22]: parser.add_argument('arg2', choices=['A','B']);
In [23]: parser.add_argument('--opt1');
In [24]: parser.add_argument('--opt2');
In [26]: args = parser.parse_args('--opt2 foo C A'.split())
In [27]: print(args)
Namespace(arg1='C', arg2='A', opt1=None, opt2='foo')
A possible test:
In [29]: if args.arg1=='C' and args.arg2=='A' and args.opt1 is None:
...: parser.error('--opt1 is required with C and A')
...:
usage: ipython3 [-h] [--opt1 OPT1] [--opt2 OPT2] {A,B,C,D} {A,B}
ipython3: error: --opt1 is required with C and A
Keep in mind that it's hard to describe conditional interactions in the usage and help. An alternative to requiring an optional is to give it a reasonable default. That way it won't matter whether the user gives a value or not. But in the above test, I'm depending on the default default None.
The usage shows the optionals first, and in the commandline optionals can occur in an order, with respect to themselves and the positionals.
Not sure I fully understood your requirement but see if this example helps you:
from argparse import ArgumentParser
parser = ArgumentParser()
subparsers = parser.add_subparsers(help="sub-commands")
a_parser = subparsers.add_parser("ARG1")
a_parser.add_argument("-1", "--opt1", action="store_true", required=False)
a_parser.add_argument("-2", "--opt2", action="store_true", required=False)
a_parser.add_argument("-3", "--opt3", action="store_true", required=False)
a_parser.add_argument("-4", "--opt4", action="store_true", required=False)
subparsers2 = a_parser.add_subparsers(help="ARG1-ARG2 sub-commands")
ab_parser = subparsers2.add_parser("ARG2")
ab_parser.add_argument("-1", "--opt1", action="store_true", required=False)
ab_parser.add_argument("-2", "--opt2", action="store_true", required=False)
b_parser = subparsers.add_parser("ARG2")
b_parser.add_argument("-3", "--opt3", action="store_true", required=False)
b_parser.add_argument("-4", "--opt4", action="store_true", required=False)
argument = parser.parse_args()
Output:
± % python recovery_tools/test.py ARG1 --help
usage: test.py ARG1 [-h] [-1] [-2] [-3] [-4] {ARG2} ...
positional arguments:
{ARG2} ARG1-ARG2 sub-commands
optional arguments:
-h, --help show this help message and exit
-1, --opt1
-2, --opt2
-3, --opt3
-4, --opt4
± % python recovery_tools/test.py ARG1 ARG2 --help
usage: test.py ARG1 ARG2 [-h] [-1] [-2]
optional arguments:
-h, --help show this help message and exit
-1, --opt1
-2, --opt2
± % python recovery_tools/test.py ARG2 --help
usage: test.py ARG2 [-h] [-3] [-4]
optional arguments:
-h, --help show this help message and exit
-3, --opt3
-4, --opt4
(new-recovery-tools-env)

Python argparse - mandatory argument - either positional or optional

I want the user to be able to pass a mandatory argument to 'argparse', with either positional, or optional argument.
I.e.,
both following forms are valid:
my_prog arg
my_prog -m arg
I've seen Argparse optional positional arguments?
But the suggestions there make both form optional. I want that one of them will be mandatory.
Of course, I can add a manual checking after parsing that at least one of them has been set. But I got a hunch that there must be a better solution.
(And even with my manual approach, the 'help' section shows both of them as optional)
The mutually exclusive group mechanism can take a required parameter. It also works with one ? positional along with the optionals (flagged arguments). (more than one '?' positional doesn't make sense).
As for the help display there are 2 default groups, positonal and optional. So even if an optional (flagged) is set to required it is, by default, displayed in the optional group. The usage line is a better guide as to whether an argument is required or not. If you don't like the group labels in the help section, define your own argument groups.
In [146]: import argparse
In [147]: parser = argparse.ArgumentParser()
In [148]: gp = parser.add_mutually_exclusive_group(required=True)
In [149]: gp.add_argument('pos', nargs='?', default='foo');
In [150]: gp.add_argument('-f','--foo', default='bar');
In [151]: parser.parse_args('arg'.split())
Out[151]: Namespace(foo='bar', pos='arg')
In [152]: parser.parse_args('-f arg'.split())
Out[152]: Namespace(foo='arg', pos='foo')
In [153]: parser.parse_args('arg -f arg'.split())
usage: ipython3 [-h] [-f FOO] [pos]
ipython3: error: argument -f/--foo: not allowed with argument pos
In [154]: parser.parse_args(''.split())
usage: ipython3 [-h] [-f FOO] [pos]
ipython3: error: one of the arguments pos -f/--foo is required
In [155]: parser.parse_args('-h'.split())
usage: ipython3 [-h] [-f FOO] [pos]
positional arguments:
pos
optional arguments:
-h, --help show this help message and exit
-f FOO, --foo FOO
Oops, usage isn't showing the -f and pos in a mutually exlusive group. Sometimes that usage formatting is brittle.
Switching the order in which the arguments are defined gives a better usage
In [156]: parser = argparse.ArgumentParser()
In [157]: gp = parser.add_mutually_exclusive_group(required=True)
In [158]: gp.add_argument('-f','--foo', default='bar');
In [159]: gp.add_argument('pos', nargs='?', default='foo');
In [160]:
In [160]: parser.parse_args('-h'.split())
usage: ipython3 [-h] (-f FOO | pos)
positional arguments:
pos
optional arguments:
-h, --help show this help message and exit
-f FOO, --foo FOO
With a user defined argument group:
In [165]: parser = argparse.ArgumentParser()
In [166]: gp = parser.add_argument_group('Mutually exclusive')
In [167]: gpm = gp.add_mutually_exclusive_group(required=True)
In [168]: gpm.add_argument('-f','--foo', default='bar');
In [169]: gpm.add_argument('pos', nargs='?', default='foo');
In [170]:
In [170]: parser.parse_args('-h'.split())
usage: ipython3 [-h] (-f FOO | pos)
optional arguments:
-h, --help show this help message and exit
Mutually exclusive:
-f FOO, --foo FOO
pos
This is the one exception to the general rule argument_groups and mutually_exclusive_groups aren't designed for nesting.
The m-x-group was not required, usage would use []
usage: ipython3 [-h] [-f FOO | pos]

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.

Use either one flag argument or two positional arguments with argparse

i'm stuck on a task, which require either 2 positional args or 1 (a file) if flag enabled:
parser.add_argument('pos_arg1', help='desc')
parser.add_argument('pos_arg2', help='desc')
parser.add_argument('--add_arg1', help='desc', type=argparse.FileType('r'), nargs='?')
so using the script with either script.py arg1 arg2 or script.py --add_arg1 file are both valid. How would I do this?
If you define:
In [422]: parser=argparse.ArgumentParser()
In [423]: g=parser.add_mutually_exclusive_group()
In [424]: g.add_argument('--foo')
In [426]: g.add_argument('bar',nargs='*',default='test')
In [427]: parser.print_help()
usage: ipython2.7 [-h] [--foo FOO | bar [bar ...]]
positional arguments:
bar
optional arguments:
-h, --help show this help message and exit
--foo FOO
In [429]: parser.parse_args([])
Out[429]: Namespace(bar='test', foo=None)
In [430]: parser.parse_args(['one','two'])
Out[430]: Namespace(bar=['one', 'two'], foo=None)
In [431]: parser.parse_args(['--foo','two'])
Out[431]: Namespace(bar='test', foo='two')
With this you can specify two (really any number) of unlabeled values, or one value flagged with --foo. It will object if I try both. I could have marked the group as required.
A couple of notes:
Marking --foo as nargs='?' is relatively meaningless unless I specify both a default and const.
I can only specify one one positional in the exclusive group, and that argument has to have '?' or '*', and a 'default'. In other words, it has to genuinely optional.
Without the mutually_exclusive_group I could make both positionals ?, but I can't say 'zero or two arguments'.

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)

Categories