Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Allow argparse nargs="+" to accept comma-separated input with choices [duplicate]

I want to use argparse's {choices} parameter, but allow the user to input any number of items from choices. For example if choices is [1,2,3], I would like the following to be valid:

--arg 1
--arg 1,2
--arg 1,3

etc.

However it seems like choices doesn't accept a comma-separated input when using nargs="+". Is there any way around this? I still want to enforce that the passed in options are within the set of choices that I defined, to error-check for weird inputs.

like image 722
jj172 Avatar asked Apr 13 '18 19:04

jj172


People also ask

What does Nargs do in Argparse?

Number of Arguments If you want your parameters to accept a list of items you can specify nargs=n for how many arguments to accept. Note, if you set nargs=1 , it will return as a list not a single value.

What does Argparse ArgumentParser () do?

ArgumentParser() initializes the parser so that you can start to add custom arguments. To add your arguments, use parser. add_argument() . Some important parameters to note for this method are name , type , and required .

What is Metavar in Argparse?

Metavar: It provides a different name for optional argument in help messages.

What does Argparse parse_args return?

The parse_args() method actually returns some data from the options specified, in this case, echo . The variable is some form of 'magic' that argparse performs for free (i.e. no need to specify which variable that value is stored in).


3 Answers

I agree - You would think that the API would allow someone to do that by now right?

Well anyways, the workaround I've always used was the following:

p = argparse.ArgumentParser(description="Why doesn't argparse support list of args?")
parser.add_argument('--arg', type=str)
arg_list = parser.parse_args().args.split(",")
# if you wanted integers:
arg_list = [int(x) for x in arg_list]

AKA: Take in a string and process it yourself.

like image 135
OneRaynyDay Avatar answered Sep 30 '22 08:09

OneRaynyDay


import argparse, sys
print(sys.argv)
parser = argparse.ArgumentParser()
parser.add_argument('--arg', nargs='+', choices=[1,2,3], type=int)
args = parser.parse_args()
print(args)

some runs

1455:~/mypy$ python stack49824248.py --arg 1 
['stack49824248.py', '--arg', '1']
Namespace(arg=[1])

1455:~/mypy$ python stack49824248.py --arg 1 3 2 1
['stack49824248.py', '--arg', '1', '3', '2', '1']
Namespace(arg=[1, 3, 2, 1])

1456:~/mypy$ python stack49824248.py --arg 1,2
['stack49824248.py', '--arg', '1,2']
usage: stack49824248.py [-h] [--arg {1,2,3} [{1,2,3} ...]]
stack49824248.py: error: argument --arg: invalid int value: '1,2'

The shell, together with the interpreter, splits the input on spaces, and provides a list of strings in sys.argv. That's what parser handles.

With +, the --arg action accepts a list of strings (to the end or next flag). Each string is passed through the type function, and the result compared to the choices (if provided). In this case, type is int, so the choices can be integers as well. Without the type, choices would have to be ['1','2','3'].

If I change the argument to:

parser.add_argument('--arg', nargs='+', choices=['1','2','3','1,2','2,3'])

it will accept some strings with commas:

1456:~/mypy$ python stack49824248.py --arg 1
['stack49824248.py', '--arg', '1']
Namespace(arg=['1'])
1505:~/mypy$ python stack49824248.py --arg 1,2
['stack49824248.py', '--arg', '1,2']
Namespace(arg=['1,2'])
1505:~/mypy$ python stack49824248.py --arg 1,2,3
['stack49824248.py', '--arg', '1,2,3']
usage: stack49824248.py [-h] [--arg {1,2,3,1,2,2,3} [{1,2,3,1,2,2,3} ...]]
stack49824248.py: error: argument --arg: invalid choice: '1,2,3' (choose from '1', '2', '3', '1,2', '2,3')

I didn't include the '1,2,3' choice, so it rejected that. Note also that I dropped the int type, since int('1,2') will fail.

So if you need to accept '1,2,3', do your own split and choices test after parsing (or possibly as a custom Action class).

In [16]: [(int(x) in [1,2,3]) for x in '1,2,3'.split(',')]
Out[16]: [True, True, True]
In [17]: [(int(x) in [1,2,3]) for x in '1,2,4'.split(',')]
Out[17]: [True, True, False]
In [18]: [(int(x) in [1,2,3]) for x in '1,a,4'.split(',')]
....
ValueError: invalid literal for int() with base 10: 'a'
like image 21
hpaulj Avatar answered Sep 30 '22 09:09

hpaulj


This one-line code gives you all possible subsets of your choices.

    from itertools import combinations, chain
    allsubsets = lambda n: list(chain(*[combinations(range(n), ni) for ni in range(n+1)]))
like image 34
Alvira Swalin Avatar answered Sep 30 '22 09:09

Alvira Swalin