Him
Him

Reputation: 5551

Python argparse check choices before type

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

Answers (2)

hpaulj
hpaulj

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

mfitzp
mfitzp

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

Related Questions