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)))
*nargs (different default), but otherwise the same point - stackoverflow.com/questions/73205632/…. Given the waychoicesare handled, there isn't a simple way to make this work.enumerateis unnecessary asiis unused). I may end up using that, since I'll likely override the usage message no matter what. The only downside is that it would break if someone specifies an argument multiple times. There's no reason for them to do so in my use case, but I prefer to be forgiving. I can probably use*instead of?and I think it'll be fine with the usage override.