2

I want to manually handle the situation where parse_args() throws an error in case of a unknown value for an argument. For example: If I have the following python file called script.py:

argp = argparse.ArgumentParser(description='example')
argp.add_argument('--compiler', choices=['default', 'clang3.4', 'clang3.5'])
args = argp.parse_args()

and I run the script with the following args python script.py --compiler=foo it throws the following error:

error: argument --compiler: invalid choice: 'foo' (choose from 'default', 'clang3.4', 'clang3.5')
SystemExit: 2

What do I need to do in order to handle this behaviour myself instead of the script quitting itself? One idea is to subclass argparse.ArgumentParser and override parse_args() or just monkey patch the method but I was wondering if there's a better way that does not require overriding the standard library behaviour?

Sid Shukla
  • 990
  • 1
  • 8
  • 33
  • How do you want to handle a different choice from your choice list? – Asish M. Aug 30 '16 at 16:00
  • @M.Klugerford doesn't matter, but to simplify, if a certain flag is passed along with a compiler value that is not in the choices list, I want to say that the scrip should be run assuming `clang3.4` as the argument. – Sid Shukla Aug 30 '16 at 16:07
  • 2
    What about creating a custom [action](https://docs.python.org/2.7/library/argparse.html#action-classes)? – FamousJameous Aug 30 '16 at 16:08
  • @FamousJameous This looks promising. Will look into it and update here. Do you have a good example for reference? – Sid Shukla Aug 30 '16 at 16:14
  • 1
    @Siddharth, Full disclosure, I have never written a custom action in `argparse`. I have done similar things in `optparse`, just never `argparse`. There is a question here: http://stackoverflow.com/questions/8632354/python-argparse-custom-actions-with-additional-arguments-passed that might help. – FamousJameous Aug 30 '16 at 16:21
  • @FamousJameous Do you suggest optparse over argparse? If so, what are the arguments? – Sid Shukla Aug 30 '16 at 16:25
  • Nvm, just found out it's deprecated. There's no point to it since this script needs to run on 3.4, 3.5, 3.6 as well as PyPy. – Sid Shukla Aug 30 '16 at 16:27
  • `optparse` won't disappear. New development in `argparse` is encouraged, but not required. – hpaulj Aug 30 '16 at 17:31
  • Unfortunately, the `__call__` method from a custom Action class gets called after the verification of choices so it does no help the situation. – Sid Shukla Aug 30 '16 at 18:04

1 Answers1

2

The whole point to defining choices is to make the parser complain about values that are not in the list. But there are some alternatives:

  • omit choices (include them in the help text if you want), and do your own testing after parsing. argparse doesn't have to do everything for you. It's main purpose is to figure out what your user wants.

  • redefine the parser.error method (via subclassing is best) to redirect the error from sys.exit. But you'll have to parse the error message to distinguish between this error and other ones that the parser might raise.

  • define a type function that checks for choices, and makes the default substitution.

The parsing of the '--compiler' option goes something like this:

  • grab the string argument after the --compiler flag
  • pass it through the type function. Default is lambda x:x. int converts it to integer, etc. Raise ValueError is value is bad.
  • check the returned value against the choices list (if any)
  • use the action to add the value to the Namespace (default simply stores it).

Error in any of these steps produces an ArgumentError which is trapped by the parser.error method and passed to a parser.exit method.

Since the store_action occurs after type and choices checking, a custom action won't bypass their errors.

Here's a possible type solution (not tested)

def compile_choices(astr):
   if astr in ['default', 'clang3.4', 'clang3.5']:
       return astr
   else:
       return 'default'
   # could raise ValueError('bad value') if there are some strings you don't like

argp.add_argument('--compiler', type=compile_choices)

=================

If compile_choices takes other arguments, such as the list of choices or the default, you'll need to wrap in some why that defines those values before parsing.

An example accepting a binary string representation:

parser.add_argument('--binary', type=lambda x: int(x, base=2),
    help='integer in binary format', default='1010')

or

parser.add_argument('--binary', type=functools.partial(int, base=2), default='1010')
hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • What if I want to pass another argument to compile_choices along with the compiler value? – Sid Shukla Aug 30 '16 at 18:20
  • Use `lambda` or `partial` or a wrapper function to provide those arguments before giving it to the parser. I added an example. – hpaulj Aug 30 '16 at 18:58