Reputation: 5551
I'm trying to enable a user to pass in a function name. For some reason it seems that argparse performs the type check/conversion BEFORE it checks the choices. Is this a bug? Best thing to do?
import argparse
def foo():
return 'foo'
def bar():
return 'bar'
parser = argparse.ArgumentParser()
functions = {f.__name__:f for f in [foo, bar]}
parser.add_argument("function", type=lambda f: functions.get(f), help="which function", choices=functions)
args = parser.parse_args()
print(args.function())
This throws:
$ python blah.py foo
usage: blah.py [-h] {foo,bar}
blah.py: error: argument function: invalid choice: <function foo at 0x7f65746dd848> (choose from 'foo', 'bar')
Upvotes: 6
Views: 2863
Reputation: 231355
Yes, during parsing the type
then choices
order is clear and intentional (and not just incidental). When preparing to assign arg_strings
to the namespace
it calls _get_values
, which does:
def _get_values(self, action, arg_strings)
.... (various nargs tests)
value = self._get_value(action, arg_string)
self._check_value(action, value)
return value
where _get_value
applies the action.type
function, and _check_value
tests
value not in action.choices
For parsing choices
only has to respond to the in
(__contains__
) expression.
So choices
have to reflect values after conversion. If type
is int
, then choices=[1,2,3]
is correct, ['1','2','3']
is not.
There are some (largely unresolved) bug issues over the display of the choices. Long lists, e.g. range(100)
work in parsing, but don't display nicely. And display also requires that choices
be iterable (e.g. a list, tuple, dictionary). This display issue affects the usage, the help and the error messages (each formats choices
slightly differently).
metavar
is your most powerful tool for replacing an undesirable choices
list. I'd have to run a test case to see whether it solves things for all 3 situations.
Upvotes: 3
Reputation: 15545
Apparently so, however you can work around this simply by using functions.keys()
as your choices, e.g.
import argparse
def foo():
return 'foo'
def bar():
return 'bar'
parser = argparse.ArgumentParser()
functions = {f.__name__:f for f in [foo, bar]}
parser.add_argument("function", type=lambda f: functions.get(f), help="which function", choices=functions.values())
args = parser.parse_args()
print(args.function())
However, if you want to provide this kind of interface (mapping functions to command line arguments) you might want to take a look at click.
Upvotes: 2