argparse funneling positional arguments into multiple lists - python

I would like to be able to support positional command line arguments that go into different lists based on prior predicates.
For example, a command like:
mycommand one two three
would yield args like:
main_dest = ['one','two','three']
other_dest = []
but a command like:
mycommand one --other two three --main four five
would yield args like:
main_dest = ['one','four','five']
other_dest = ['two','three']
Conceptually what I'd like is an action that modifies the dest of the positional argument reader.

As a first try this set of Actions seems to do the trick:
In [73]: parser = argparse.ArgumentParser()
In [74]: parser.add_argument('main', nargs='*');
In [75]: parser.add_argument('other', nargs='*');
In [76]: parser.add_argument('--main', action='append');
In [77]: parser.add_argument('--other', action='append');
In [78]: parser.print_usage()
usage: ipython3 [-h] [--main MAIN] [--other OTHER]
[main [main ...]] [other [other ...]]
In [79]: parser.parse_args('one two three'.split())
Out[79]: Namespace(main=['one', 'two', 'three'], other=[])
In [80]: parser.parse_args('one --other two --main three'.split())
Out[80]: Namespace(main=['one', 'three'], other=['two'])
74 and 76 both have main as their dest. I use append for the flagged ones so they don't overwrite positional values. But despite what the usage shows, positionals will only work at the start. If placed a the end they'll overwrite flagged values. And the 'other' positional will never get values - so I should have omitted it.
So it is possible to play games like this, but I'm not sure it's robust, or any easier for your users.
argparse: flatten the result of action='append'

Related

Find the order of arguments in argparse python3

I have this code
parser = argparse.ArgumentParser()
parser.add_argument('--input')
parser.add_argument('--min')
parser.add_argument('--max')
args = parser.parse_args()
How can I know the order of arguments ? For example, if I type:
python somefile.py --min min --input file.csv
How can I know position of --min is before --input and user didn't type --max?
I think it might be tricky to obtain that information from parser itself, but it can be easily obtained from sys.argv:
def get_arg_index(args: list, name: str):
return next((i for i, v in enumerate(args) if v.startswith(name)), None)
print(get_arg_index(sys.argv, "--min"))
print(get_arg_index(sys.argv, "--max"))
By design argparse is supposed to handle the optionals in any order. They are parsed in the order that they are provided in the command line, but it does not keep any record of that order. positionals without a keyword are handled in the order that they are defined by the add_argument definitions.
All attributes are added to the args Namespace with their defaults at the start of parsing. And it does this in the add_argument order. In newer Pythons this order is preserved in the args.__dict__ (which vars(args) also shows).
But in print(args), the attributes are displayed in sorted order. usage also preserves the definition order for optionals.
So for a sample parser:
In [11]: p.print_usage()
usage: ipython3 [-h] [--foo FOO] [--test TEST] [--aaa AAA] bar
In [12]: args=p.parse_args(['xxx'])
In [13]: args
Out[13]: Namespace(aaa=None, bar='xxx', foo=None, test=None) # sorted
In [14]: vars(args)
Out[14]: {'foo': None, 'bar': 'xxx', 'test': None, 'aaa': None} # definition
In [15]: [a.dest for a in p._actions]
Out[15]: ['help', 'foo', 'bar', 'test', 'aaa'] # definition order
So recording the order of occurrence of the arguments in the command line is, with just argparse, awkward. In a sense it goes against the spirit of optionals.
Alternatives:
work directly with the sys.argv
customize the action class
customize the Namespace class
One other trick comes to mind:
If the default is argparse.SUPPRESS, the default is not added to args. Then I think the var(args) order will be the order in which values are parsed and added.
In [16]: for a in p._actions: a.default=argparse.SUPPRESS
In [24]: args=p.parse_args(['--test', 'ttt','xxx','--foo=3'])
In [25]: args
Out[25]: Namespace(bar='xxx', foo='3', test='ttt')
In [26]: vars(args)
Out[26]: {'test': 'ttt', 'bar': 'xxx', 'foo': '3'}
Users may repeat optionals, with repeats over writing previous values.

Use parser.parse_args() with nargs and args rather than command line input

I have an argparse argument example that accepts a variable number of strings:
parser.add_argument('--example', nargs='*', required=False, default='')
This works fine when using the CLI as usual with command-line input: script.py --example ab cd.
I also want this to work with arguments that are passed to parser.parse_args(args) through the args argument from within the program code.
But when I try to pass parser.parse_args(["--example", "ab cd"]), it's interpreted as single string "ab cd" and not as a list of ab and cd.
Similarly, passing parser.parse_args(["--example", "ab", "cd"]) is somehow interpreted as single argument, which is a list. So, when accessing args.example[0] I get the list and args.example[1] gets me an error.
As your link shows, you can give parse_args a list of strings.
In [210]: parser = argparse.ArgumentParser()
In [211]: parser.add_argument('-i','--image_types', nargs='*', default='');
To test parsing without any commandline arguments, by giving it an empty list:
In [212]: parser.parse_args([])
Out[212]: Namespace(image_types='')
With a list of strings:
In [213]: parser.parse_args(['-i','a','b'])
Out[213]: Namespace(image_types=['a', 'b'])
or split a string:
In [214]: parser.parse_args('-i a b'.split())
Out[214]: Namespace(image_types=['a', 'b'])
Answers to argparse questions often use one of these forms to illustrate their actions. The split is convenient.
It is also possible to create an args Namespace directly:
In [215]: argparse.Namespace(image_types=['a','b','c'])
Out[215]: Namespace(image_types=['a', 'b', 'c'])
I don't follow your interpretation of the result for
parser.parse_args(["--example", "ab", "cd"])
That should produce a args.example that is ["ab", "cd"].
This isn't a good test case:
parser.parse_args(["--example", "ab cd"])
to produce the same thing from the commandline you'd have to use
python --example "ab cd"
The quotes override the normal split on white space. You have to use shlex.split to emulate that behavior.
It is tricky for argparse to handle lists directly use a csv list instead:
parser.add_argument('--image_types', \
help='csv list of imagetypes', \
default='iff,gif,jpeg,png', default=None)
if args.image_types:
args.image_types = args.image_types.split(',')

Attribution of command line parameter to multiple arguments

I am trying to build a command line parser that will be able to share between arguments the values passed in order to avoid having to type them multiple times. Said otherwise, I would like the namespaces of both argument to be identical:
import argparse
class PrintAction(argparse.Action):
def __init__(self, option_strings, dest, **kwargs):
super(PrintAction, self).__init__(option_strings, dest, **kwargs)
def __call__(self, parser, namespace, values, option_string=None):
for val in values:
print(val)
parser = argparse.ArgumentParser(description='A foo that foos and a bar that bars')
parser.add_argument('--foo', action=PrintAction)
parser.add_argument('bar', nargs='+')
args = parser.parse_args(['--foo', 'a', 'b', 'c']) # Case 1
args = parser.parse_args(['a', 'b', 'c']) # Case 2
I would then like a solution that stores in both cases ['a', 'b', 'c'] in bar but also that in the case that --foo is provided, then a, b and c would be printed.
For now, what I get is foo prints only a and bar stores only b and c in case 1 and the correct result in case 2.
You need to make --foo a boolean flag. Now it's a string parameter, because you did not state otherwise. Set action to store_true for the boolean flag effect.
The final solution would look like:
def print_args(args):
if args.foo:
for val in args.bar:
print(val)
parser = argparse.ArgumentParser(description='A foo that foos and a bar that bars')
parser.add_argument('--foo', action='store_true')
parser.add_argument('bar', nargs='+')
args = parser.parse_args(['--foo', 'a', 'b', 'c']) # Case 1
args = parser.parse_args(['a', 'b', 'c']) # Case 2
Then calling print_args(args) in the first case will print a, b and c and in the second case, it won't.
You can't (readily) trick the argparse into reusing argv strings. The parser allocates values to the Actions.
The default nargs is None, which means, use the the next string as an argument.
parser.add_argument('--foo')
would set foo='a', and bar=['b','c'].
In your Action, values will be ['a'], which you print. In optparse each option gets the remaining argv list, which it can consume as it wants. In argparse it only gets the values that its nargs demands.
You could specify in the __init__ that the nargs=0, and then print from sys.argv. Eqivalently, as #9000 suggests, make it a store_true and print after parsing. Look at the code for the store_true Action class.
Another option is to give both foo and bar a *, and have foo both print and save to the bar dest. Then foo would consume all following strings. But, if bar doesn't have anything to save, it might write [] to the namespace.
In any case, the best you can do is fake the repeated use.
Another idea is to use 2 different parsers with parse_known_args. Parsers don't mess with the sys.argv, so it can read and parsed multiple times.

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

Categories

Resources