Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python argparse: Combine optional parameters with nargs=argparse.REMAINDER

I must be missing something obvious. The goal is to use argparse with the first parameter required, a second optional and any other remaining parameters optional.

To show the issue I made two test parsers; the only difference between them is using nargs=argparse.REMAINDER in one and nargs='*' in the other.

def doParser1(argsin):
    parser = argparse.ArgumentParser(description='Parser demo.')
    parser.add_argument('req1', help='first required parameter')
    parser.add_argument('--opt1', help='first optional parameter')
    parser.add_argument('leftovers', nargs=argparse.REMAINDER,
                    help='all the other parameters')
    argsout = parser.parse_args(args=argsin)
    print argsout
    return argsout

def doParser2(argsin):
    parser = argparse.ArgumentParser(description='Parser demo.')
    parser.add_argument('req1', help='first required parameter')
    parser.add_argument('--opt1', help='first optional parameter')
    parser.add_argument('leftovers', nargs='*',
                    help='all the other parameters')
    argsout = parser.parse_args(args=argsin)
    print argsout
    return argsout

If there are no extra parameters, parser2 works. This is the input followed by parser1 and parser 1:

input: ['req1value', '--opt1', 'opt1value']
Namespace(leftovers=['--opt1', 'opt1value'], opt1=None, req1='req1value')
Namespace(leftovers=None, opt1='opt1value', req1='req1value')

If there are extra parameters, the opt1 value is missed in parser1 and parser2 just gets confused:

input: ['req1value', '--opt1', 'opt1value', 'r1', 'r2']
Namespace(leftovers=['--opt1', 'opt1value', 'r1', 'r2'], opt1=None, req1='req1value')
usage: py-argparse.py [-h] [--opt1 OPT1]
                  [-leftovers [LEFTOVERS [LEFTOVERS ...]]]
                  req1
py-argparse.py: error: unrecognized arguments: r1 r2

The expected output should be:

Namespace(leftovers=['r1', 'r2'], opt1='opt1value', req1='req1value')

It seems this should be a simple case and what is here is simplified from what I'm really trying to do. I've tried making leftovers optional, adding a variety of other options, but nothing works any better.

Any help would be appreciated.

like image 321
OPunWide Avatar asked Sep 04 '13 20:09

OPunWide


People also ask

What does Nargs do in Argparse?

Using the nargs parameter in add_argument() , you can specify the number (or arbitrary number) of inputs the argument should expect. In this example named sum.py , the --value argument takes in 3 integers and will print the sum.

How do I make Argparse argument optional in Python?

Python argparse optional argument The example adds one argument having two options: a short -o and a long --ouput . These are optional arguments. The module is imported. An argument is added with add_argument .

What does Argparse ArgumentParser ()?

The ArgumentParser.parse_args() method runs the parser and places the extracted data in a argparse.Namespace object: args = parser. parse_args() print(args.

What is Metavar in Python Argparse?

Metavar: It provides a different name for optional argument in help messages. Provide a value for the metavar keyword argument within add_argument() .


3 Answers

You could use parse_known_args:

import argparse
parser = argparse.ArgumentParser(description='Parser demo.')
parser.add_argument('req1', help='first required parameter')
parser.add_argument('--opt1', help='first optional parameter')

args, leftovers = parser.parse_known_args(['req1value', '--opt1', 'opt1value'])
print(args, leftovers)
# (Namespace(opt1='opt1value', req1='req1value'), [])

args, leftovers = parser.parse_known_args(['req1value', '--opt1', 'opt1value', 'r1', 'r2'])
print(args, leftovers)
# (Namespace(opt1='opt1value', req1='req1value'), ['r1', 'r2'])
like image 68
unutbu Avatar answered Oct 22 '22 22:10

unutbu


--opt1 needs to come before "unnamed" arguments. Your real test cases should be:

    ['--opt1', 'opt1value', 'req1value']

and

    ['--opt1', 'opt1value', 'req1value', 'r1', 'r2']
like image 31
Mario Rossi Avatar answered Oct 22 '22 20:10

Mario Rossi


The inter mixing of positionals and optionals is tricky when one or more of the positionals is of the 'zero or more' type (? * REMAINDER). The simple solution is to not mix them - give the optionals first, then all the positionals afterwards.

Here's what's going on:

input: ['req1value', '--opt1', 'opt1value']
Namespace(leftovers=['--opt1', 'opt1value'], opt1=None, req1='req1value')

Because of the req1value string the parser first parses for positionals. req1 wants 1 string, leftovers grabs everything else including --opt1.

Namespace(leftovers=None, opt1='opt1value', req1='req1value')

With * leftovers is satisfied with [], no string, hence None (actually I get []). --opt1 is parsed as an optional.

input: ['req1value', '--opt1', 'opt1value', 'r1', 'r2']
...
py-argparse.py: error: unrecognized arguments: r1 r2

As before * leftovers is set to []. -opt1 is processed. But now there are 2 strings with no place to put them. You intended them to go into leftovers, but that was already used. If leftovers was + it would have taken them as you intended.

The key is that when it tries to parse the 1st positional, it also tries to parse all the positionals that it can. At one level parse_args is doing re.match('(A)(A*)','AOA') producing groups ('A', '').

There are 2 proposed patches that deal with this issue. One uses the 2 step parse_known_args to allow a complete mixing of optionals and positionals. This is the kind of behavior that users of optparse might have come to expect.

The other patch tries to delay the handling of positionals that can accept 0 argument strings http://bugs.python.org/issue15112 .

like image 45
hpaulj Avatar answered Oct 22 '22 22:10

hpaulj