My use case is multiple optional positional arguments, taken from a constrained set of choices
, with a default
value that is a list containing two of those choices. I can't change the interface, due to backwards compatibility issues. I also have to maintain compatibility with Python 3.4.
Here is my code. You can see that I want my default to be a list of two values from the set of choices
.
parser = argparse.ArgumentParser()
parser.add_argument('tests', nargs='*', choices=['a', 'b', 'c', 'd'],
default=['a', 'd'])
args = parser.parse_args()
print(args.tests)
All of this is correct:
$ ./test.py a
['a']
$ ./test.py a d
['a', 'd']
$ ./test.py a e
usage: test.py [-h] [{a,b,c,d} ...]
test.py: error: argument tests: invalid choice: 'e' (choose from 'a', 'b', 'c', 'd')
This is incorrect:
$ ./test.py
usage: test.py [-h] [{a,b,c,d} ...]
test.py: error: argument tests: invalid choice: ['a', 'd'] (choose from 'a', 'b', 'c', 'd')
I've found a LOT of similar questions but none that address this particular use case. The most promising suggestion I've found (in a different context) is to write a custom action and use that instead of choices
:
That's not ideal. I'm hoping someone can point me to an option I've missed.
Here's the workaround I plan to use if not:
parser.add_argument('tests', nargs='*',
choices=['a', 'b', 'c', 'd', 'default'],
default='default')
I'm allowed to add arguments as long as I maintain backwards compatibility.
Thanks!
Update: I ended up going with a custom action. I was resistant because this doesn't feel like a use case that should require custom anything. However, it seems like more or less the intended use case of subclassing argparse.Action
, and it makes the intent very explicit and gives the cleanest user-facing result I've found.
class TestsArgAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
all_tests = ['a', 'b', 'c', 'd']
default_tests = ['a', 'd']
if not values:
setattr(namespace, self.dest, default_tests)
return
# If no argument is specified, the default gets passed as a
# string 'default' instead of as a list ['default']. Probably
# a bug in argparse. The below gives us a list.
if not isinstance(values, list):
values = [values]
tests = set(values)
# If 'all', is found, replace it with the tests it represents.
# For reasons of compatibility, 'all' does not actually include
# one of the tests (let's call it 'e'). So we can't just do
# tests = all_tests.
try:
tests.remove('all')
tests.update(set(all_tests))
except KeyError:
pass
# Same for 'default'
try:
tests.remove('default')
tests.update(set(default_tests))
except KeyError:
pass
setattr(namespace, self.dest, sorted(list(tests)))