argparse: how to configure multiple choice, multiple value, optional argument? - python

I'm trying to set up an argument that accepts one or more values from a given list of choices, but is not obligatory. I'm trying this (with a couple of variants that also don't work as expected):
parser.add_argument("FLAGS", nargs='*', choices=["X","Y","Z","ALL"])
I expect to get a list of values from the list of choices, or an empty list if nothing was given (that, I think, should be enforced by nargs='*'). But regardless of whether I add default="" or not, when I don't pass any argument it fails with:
error: argument FLAGS: invalid choice: []
How to achieve what I need?

This probably doesn't fit your needs, but you can do it easily with an option like --flags.
parser.add_argument(
"--flags",
nargs='*',
default=[], # Instead of "None"
choices=["X", "Y", "Z", "ALL"])
args = parser.parse_args()
print(args)
$ tmp.py
Namespace(flags=[])
$ tmp.py --flags
Namespace(flags=[])
$ tmp.py --flags X
Namespace(flags=['X'])
$ tmp.py --flags X Z
Namespace(flags=['X', 'Z'])
$ tmp.py --flags foobar
usage: tmp.py [-h] [--flags [{X,Y,Z,ALL} ...]]
tmp.py: error: argument --flags: invalid choice: 'foobar' (choose from 'X', 'Y', 'Z', 'ALL')
$ tmp.py --help
usage: tmp.py [-h] [--flags [{X,Y,Z,ALL} ...]]
optional arguments:
-h, --help show this help message and exit
--flags [{X,Y,Z,ALL} ...]

A positional '*' gets some special handling. Its nargs is satisfied with an empty list (nothing). It's always handled. What does it mean to check an empty list of strings against the choices?
So the get_values() method does:
# when nargs='*' on a positional, if there were no command-line
# args, use the default if it is anything other than None
elif (not arg_strings and action.nargs == ZERO_OR_MORE and
not action.option_strings):
if action.default is not None:
value = action.default
else:
value = arg_strings
self._check_value(action, value)
where _check_value tests if value is in the choices.
Such a positional is best used with a valid default.
In [729]: p=argparse.ArgumentParser()
In [730]: a=p.add_argument("FLAGS", nargs='*', choices=["X","Y","Z","ALL"])
In [731]: p.parse_args([])
usage: ipython3 [-h] [{X,Y,Z,ALL} [{X,Y,Z,ALL} ...]]
ipython3: error: argument FLAGS: invalid choice: [] (choose from 'X', 'Y', 'Z', 'ALL')
...
Testing an empty list against choices fails:
In [732]: a.choices
Out[732]: ['X', 'Y', 'Z', 'ALL']
In [733]: [] in a.choices
Out[733]: False
In [734]: 'X' in a.choices
Out[734]: True
If we set a valid default:
In [735]: a.default='X'
In [736]: p.parse_args([])
Out[736]: Namespace(FLAGS='X')
This behavior is part of what allows us to use such a positional in a mutually_exclusive_group.
If you don't want to specify a valid default, then changing this to a flagged argument avoids the problem.

Related

How to use optional positional arguments with nargs='*' arguments in argparse?

As shown in the following code, I want to have an optional positional argument files, I want to specify a default value for it, when paths are passed in, use specified path.
But because --bar can have multiple arguments, the path passed in didn't go into args.files, how do I fix that? Thanks!
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--foo')
parser.add_argument('--bar', nargs='*')
parser.add_argument('files', nargs='?')
cmd = '--foo a --bar b c d '
print parser.parse_args(cmd.split())
# Namespace(bar=['b', 'c', 'd'], files=None, foo='a')
cmd = '--foo a --bar b c d /path/to/file1'
print parser.parse_args(cmd.split())
# Namespace(bar=['b', 'c', 'd', '/path/to/file1'], files=None, foo='a')
Your argument spec is inherently ambiguous (since --bar can take infinite arguments, there is no good way to tell when it ends, particularly since files is optional), so it requires user disambiguation. Specifically, argparse can be told "this is the end of the switches section, all subsequent argument are positional" by putting -- before the positional only section. If you do:
cmd = '--foo a --bar b c d -- /path/to/file1'
print parser.parse_args(cmd.split())
You should get:
Namespace(bar=['b', 'c', 'd'], files='/path/to/file1', foo='a')
(Tested on Py3, but should apply to Py2 as well)
Alternatively, the user can pass the positional argument anywhere it's unambiguous by avoiding putting positional arguments after --bar e.g.:
cmd = '/path/to/file1 --foo a --bar b c d'
or
cmd = '--foo a /path/to/file1 --bar b c d'
Lastly, you could avoid using nargs='*' for switches, given the ambiguity it introduces. Instead, define --bar to be accepted multiple times with a single value per switch, accumulating all uses to a list:
parser.add_argument('--bar', action='append')
then you pass --bar multiple times to supply multiple values one at a time, instead of passing it once with many values:
cmd = '--foo a --bar b --bar c --bar d /path/to/file1'

Accept exactly 0, 1 or 2 positional arguments [duplicate]

This question already has an answer here:
Python argparse: Is there a way to specify a range in nargs?
(1 answer)
Closed 6 years ago.
I'm using argparse package to parse command line arguments. Now I want to accept exactly 0, 1 or 2 strings and put them into a list.
My current approach looks like:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('strs', nargs='*')
print(parser.parse_args([]).strs)
print(parser.parse_args(['AAA']).strs)
print(parser.parse_args(['AAA', 'BBB']).strs)
print(parser.parse_args(['AAA', 'BBB', 'CCC']).strs)
For the 1st, 2nd and 3rd parse_args() the results are expected. For the last one my current approach is if len(parse_args.strs) > 2: raise. Is there any better approach that could let argparse check for me?
There isn't a means in argparse to accept a range of nargs, just values like 1,2,'+','*'.
You could write a custom Action class that would check the number of values that the nargs='*' passes it. But I think it would be just as easy to perform that test after parsing.
I could find a bug/issue that explores adding a range nargs option if you really want to get into that.
If you didn't have other positionals you could define 3 positionals, 2 of them with ?. You might even be able to combine them into one list with a common name and 'append' action. I'll explore that.
In [573]: p=argparse.ArgumentParser()
In [574]: p.add_argument('str',action='append')
In [575]: p.add_argument('str',nargs='?',action='append')
In [576]: p.add_argument('str',nargs='?',action='append')
In [577]: p.print_help()
usage: ipython3 [-h] str [str] [str]
positional arguments:
str
str
str
optional arguments:
-h, --help show this help message and exit
In [578]: p.parse_args('1 2 3'.split())
Out[578]: Namespace(str=['1', '2', '3'])
In [579]: p.parse_args('1 3'.split())
Out[579]: Namespace(str=['1', '3', None])
In [580]: p.parse_args('1'.split())
Out[580]: Namespace(str=['1', None, None])
Got the default Nones. I can correct that with
p.add_argument('str', nargs='?', action='append', default=argparse.SUPPRESS)
In [586]: p.parse_args('1 2'.split())
Out[586]: Namespace(str=['1', '2'])
In [588]: p.parse_args('1 2 3 4'.split())
usage: ipython3 [-h] str [str] [str]
ipython3: error: unrecognized arguments: 4
(oops, this is coded for 1-3 argument, but you get the idea).

Using the same option multiple times in Python's argparse

I'm trying to write a script that accepts multiple input sources and does something to each one. Something like this
./my_script.py \
-i input1_url input1_name input1_other_var \
-i input2_url input2_name input2_other_var \
-i input3_url input3_name
# notice inputX_other_var is optional
But I can't quite figure out how to do this using argparse. It seems that it's set up so that each option flag can only be used once. I know how to associate multiple arguments with a single option (nargs='*' or nargs='+'), but that still won't let me use the -i flag multiple times. How do I go about accomplishing this?
Just to be clear, what I would like in the end is a list of lists of strings. So
[["input1_url", "input1_name", "input1_other"],
["input2_url", "input2_name", "input2_other"],
["input3_url", "input3_name"]]
Here's a parser that handles a repeated 2 argument optional - with names defined in the metavar:
parser=argparse.ArgumentParser()
parser.add_argument('-i','--input',action='append',nargs=2,
metavar=('url','name'),help='help:')
In [295]: parser.print_help()
usage: ipython2.7 [-h] [-i url name]
optional arguments:
-h, --help show this help message and exit
-i url name, --input url name
help:
In [296]: parser.parse_args('-i one two -i three four'.split())
Out[296]: Namespace(input=[['one', 'two'], ['three', 'four']])
This does not handle the 2 or 3 argument case (though I wrote a patch some time ago for a Python bug/issue that would handle such a range).
How about a separate argument definition with nargs=3 and metavar=('url','name','other')?
The tuple metavar can also be used with nargs='+' and nargs='*'; the 2 strings are used as [-u A [B ...]] or [-u [A [B ...]]].
This is simple; just add both action='append' and nargs='*' (or '+').
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-i', action='append', nargs='+')
args = parser.parse_args()
Then when you run it, you get
In [32]: run test.py -i input1_url input1_name input1_other_var -i input2_url i
...: nput2_name input2_other_var -i input3_url input3_name
In [33]: args.i
Out[33]:
[['input1_url', 'input1_name', 'input1_other_var'],
['input2_url', 'input2_name', 'input2_other_var'],
['input3_url', 'input3_name']]
-i should be configured to accept 3 arguments and to use the append action.
>>> p = argparse.ArgumentParser()
>>> p.add_argument("-i", nargs=3, action='append')
_AppendAction(...)
>>> p.parse_args("-i a b c -i d e f -i g h i".split())
Namespace(i=[['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']])
To handle an optional value, you might try using a simple custom type. In this case, the argument to -i is a single comma-delimited string, with the number of splits limited to 2. You would need to post-process the values to ensure there are at least two values specified.
>>> p.add_argument("-i", type=lambda x: x.split(",", 2), action='append')
>>> print p.parse_args("-i a,b,c -i d,e -i g,h,i,j".split())
Namespace(i=[['a', 'b', 'c'], ['d', 'e'], ['g', 'h', 'i,j']])
For more control, define a custom action. This one extends the built-in _AppendAction (used by action='append'), but just does some range checking on the number of arguments given to -i.
class TwoOrThree(argparse._AppendAction):
def __call__(self, parser, namespace, values, option_string=None):
if not (2 <= len(values) <= 3):
raise argparse.ArgumentError(self, "%s takes 2 or 3 values, %d given" % (option_string, len(values)))
super(TwoOrThree, self).__call__(parser, namespace, values, option_string)
p.add_argument("-i", nargs='+', action=TwoOrThree)
Adding with Other in this Thread.
If you use action='append' in add_argument() then you will get arguments in list(s) within a list every time you add the option.
As you liked:
[
["input1_url", "input1_name", "input1_other"],
["input2_url", "input2_name", "input2_other"],
["input3_url", "input3_name"]
]
But if anyone wants those arguments in the same list[], then use action='extend' instead of  action='append' in your code. This will give you those arguments in a single list.
[
"input1_url",
"input1_name",
"input1_other",
"input2_url",
"input2_name",
"input2_other",
"input3_url",
"input3_name"
]

ArgumentParser: Optional argument with optional value

If I have an optional argument with optional argument value, is there a way to validate if the argument is set when the value is not given?
For instance:
parser = argparse.ArgumentParser()
parser.add_argument('--abc', nargs='?')
args = parser.parse_args()
Would correctly give me:
optional arguments:
--abc [ABC]
How do I distinguish between 1 and 2 below?
'' => args.abc is None
'--abc' => args.abc is still None
'--abc something' => args.abc is something
...
Update:
Found a trick to solve this problem: you can use "nargs='*'" instead of "nargs='?'". This way #1 would return None, and #2 would return an empty list. The downside is this will allow multiple values for the arguments to be accepted too; so you'd need to add a check for it if appropriate.
Alternatively you can also set a default value for the argument; see answer from chepner and Anand S Kumar.
With nargs='?', you can supply both a default and const.
In [791]: parser=argparse.ArgumentParser()
In [792]: parser.add_argument('--abc', nargs='?', default='default', const='const')
If the argument is not given it uses the default:
In [793]: parser.parse_args([])
Out[793]: Namespace(abc='default')
If given, but without an argument string, it uses the const:
In [794]: parser.parse_args(['--abc'])
Out[794]: Namespace(abc='const')
Otherwise it uses the argument string:
In [795]: parser.parse_args(['--abc','test'])
Out[795]: Namespace(abc='test')
In [796]: parser.print_help()
usage: ipython3 [-h] [--abc [ABC]]
optional arguments:
-h, --help show this help message and exit
--abc [ABC]
Use a different default value for the option. Compare
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--abc', nargs='?', default="default")
>>> parser.parse_args()
Namespace(abc='default')
>>> parser.parse_args(['--abc'])
Namespace(abc=None)
>>> parser.parse_args(['--abc', 'value'])
Namespace(abc='value')
I'm not sure how you would provide a different value for when --abc is used without an argument, short of using a custom action instead of the nargs argument.
Not sure if this is the standard way, but you can set default argument to something , and then that value would be used in case --abc is not in the argument list.
Example code -
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--abc', nargs='?', default="-1")
args = parser.parse_args()
print(args)
Result -
>python a.py
Namespace(abc='-1')
>python a.py --abc
Namespace(abc=None)
>python a.py --abc something
Namespace(abc='something')
I'm using this to have a command-line arg for multiprocessing. Specifying --multi uses all cores and given an arg specifies a number of cores, e.g., --multi 4 for four cores.
parser.add_argument("-mp", "--multi", type=int, nargs="*", help=multi_text)
Parsing logic is then:
if (args.multi == None):
num_cores = 1
elif (args.multi == []):
num_cores = multiprocessing.cpu_count()
elif (len(args.multi) == 1):
num_cores = args.multi[0]
else:
print("Invalid specification of core usage.")
sys.exit(1)

argparse - python

In a python script I want to have three positional arguments and two optional arguments(including 'help'). So my need is like following
Correct:
./myscript.py ONE TWO THREE
./myscript.py --list
Incorrect:
./myscript.py ONE TWO THREE --list
I want to make all positional argument and the optional argument as mutual exclusive using argparse itself.
This approxmates what you want:
class Three(argparse.Action):
# custom action that requires 0 or 3 values
def __call__(self,parser,namespace,values,option_string):
if len(values) in [0,3]:
setattr(namespace, self.dest, values)
else:
raise argparse.ArgumentError(self,'must have 3 values')
custom = 'usage: %(prog)s [-h] (--list | ONE TWO THREE)'
p=argparse.ArgumentParser(prog='PROG',usage=custom)
g=p.add_mutually_exclusive_group(required=True)
g.add_argument('--list',action='store_true')
g.add_argument('pos',nargs='*',default=[],action=Three)
It raises an error if both --list and pos are given, if nothing is given, or the number of pos values is not 3. I included a custom usage since the default is:
usage: PROG [-h] (--list | pos [pos ...])

Categories

Resources