Home > Enterprise >  argparse: how to configure multiple choice, multiple value, optional argument?
argparse: how to configure multiple choice, multiple value, optional argument?

Time:12-16

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?

CodePudding user response:

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} ...]

CodePudding user response:

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