20

I have a script where I ask the user for a list of pre-defined actions to perform. I also want the ability to assume a particular list of actions when the user doesn't define anything. however, it seems like trying to do both of these together is impossible.

when the user gives no arguments, they receive an error that the default choice is invalid

acts = ['clear','copy','dump','lock']
p = argparse.ArgumentParser()
p.add_argument('action', nargs='*', action='append', choices=acts, default=[['dump', 'clear']])
args = p.parse_args([])
>>> usage: [-h] [{clear,copy,dump,lock} [{clear,copy,dump,lock} ...]]
: error: argument action: invalid choice: [['dump', 'clear']] (choose from 'clear', 'copy', 'dump', 'lock')

and when they do define a set of actions, the resultant namespace has the user's actions appended to the default, rather than replacing the default

acts = ['clear','copy','dump','lock']
p = argparse.ArgumentParser()
p.add_argument('action', nargs='*', action='append', choices=acts, default=[['dump', 'clear']])
args = p.parse_args(['lock'])
args
>>> Namespace(action=[['dump', 'clear'], ['dump']])
Ciro Santilli OurBigBook.com
  • 347,512
  • 102
  • 1,199
  • 985
Steve
  • 534
  • 1
  • 4
  • 13
  • I'm not sure if this has been resolved by a similar bug was report: http://bugs.python.org/issue9625. One possible way to handle this is to use a custom action, rather than the `choices` keyword. See the accepted answer on [this question](http://stackoverflow.com/questions/4194948/python-argparse-is-there-a-way-to-specify-a-range-in-nargs) – Chris Dec 15 '11 at 22:04

5 Answers5

17

What you need can be done using a customized argparse.Action as in the following example:

import argparse

parser = argparse.ArgumentParser()

class DefaultListAction(argparse.Action):
    CHOICES = ['clear','copy','dump','lock']
    def __call__(self, parser, namespace, values, option_string=None):
        if values:
            for value in values:
                if value not in self.CHOICES:
                    message = ("invalid choice: {0!r} (choose from {1})"
                               .format(value,
                                       ', '.join([repr(action)
                                                  for action in self.CHOICES])))

                    raise argparse.ArgumentError(self, message)
            setattr(namespace, self.dest, values)

parser.add_argument('actions', nargs='*', action=DefaultListAction,
                    default = ['dump', 'clear'],
                    metavar='ACTION')

print parser.parse_args([])
print parser.parse_args(['lock'])

The output of the script is:

$ python test.py 
Namespace(actions=['dump', 'clear'])
Namespace(actions=['lock'])
jcollado
  • 39,419
  • 8
  • 102
  • 133
6

I ended up doing the following:

  • no append
  • add the empty list to the possible choices or else the empty input breaks
  • without default
  • check for an empty list afterwards and set the actual default in that case

Example:

parser = argparse.ArgumentParser()
parser.add_argument(
    'is',
    type=int,
    choices=[[], 1, 2, 3],
    nargs='*',
)

args = parser.parse_args(['1', '3'])
assert args.a == [1, 3]

args = parser.parse_args([])
assert args.a == []
if args.a == []:
    args.a = [1, 2]

args = parser.parse_args(['1', '4'])
# Error: '4' is not valid.
Ciro Santilli OurBigBook.com
  • 347,512
  • 102
  • 1,199
  • 985
6

In the documentation (http://docs.python.org/dev/library/argparse.html#default), it is said :

For positional arguments with nargs equal to ? or *, the default value is used when no command-line argument was present.

Then, if we do :

acts = ['clear','copy','dump','lock']
p = argparse.ArgumentParser()
p.add_argument('action', nargs='*', choices=acts, default='clear')    
print p.parse_args([])

We get what we expect

Namespace(action='clear')

The problem is when you put a list as a default. But I've seen it in the doc,

parser.add_argument('bar', nargs='*', default=[1, 2, 3], help='BAR!')

So, I don't know :-(

Anyhow, here is a workaround that does the job you want :

import sys, argparse
acts = ['clear','copy','dump','lock']
p = argparse.ArgumentParser()
p.add_argument('action', nargs='*', choices=acts)
args = ['dump', 'clear'] # I set the default here ... 
if sys.argv[1:]:
    args = p.parse_args()
print args
n1r3
  • 8,593
  • 3
  • 18
  • 19
4

You could test whether the user is supplying actions (in which case parse it as a required, position argument), or is supplying no actions (in which case parse it as an optional argument with default):

import argparse
import sys

acts = ['clear', 'copy', 'dump', 'lock']
p = argparse.ArgumentParser()
if sys.argv[1:]:
    p.add_argument('action', nargs = '*', choices = acts)
else:
    p.add_argument('--action', default = ['dump', 'clear'])

args = p.parse_args()
print(args)

when run, yields these results:

% test.py 
Namespace(action=['dump', 'clear'])
% test.py lock
Namespace(action=['lock'])
% test.py lock dump
Namespace(action=['lock', 'dump'])

You probably have other options to parse as well. In that case, you could use parse_known_args to parse the other options, and then handle the unknown arguments in a second pass:

import argparse

acts = ['clear', 'copy', 'dump', 'lock']
p = argparse.ArgumentParser()
p.add_argument('--foo')
args, unknown = p.parse_known_args()
if unknown:
    p.add_argument('action', nargs = '*', choices = acts)
else:
    p.add_argument('--action', default = ['dump', 'clear'])

p.parse_args(unknown, namespace = args)
print(args)

when run, yields these results:

% test.py 
Namespace(action=['dump', 'clear'], foo=None)
% test.py --foo bar
Namespace(action=['dump', 'clear'], foo='bar')
% test.py lock dump
Namespace(action=['lock', 'dump'], foo=None)
% test.py lock dump --foo bar
Namespace(action=['lock', 'dump'], foo='bar')
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
1

The action was being appended because of the "action='append'" parameter you passed to argparse.

After removing this parameter, the arguments passed by a user would be displayed on their own, but the program would throw an error when no arguments were passed.

Adding a '--' prefix to the first parameter resolves this in the laziest way.

acts = ['clear','copy','dump','lock']
p = argparse.ArgumentParser()
p.add_argument('--action', nargs='*', choices=acts, default=[['dump', 'clear']])
args = p.parse_args()

The downside to this approach is that the options passed by the user must now be preceded by '--action', like:

app.py --action clear dump copy
Condiment
  • 180
  • 4