classification
Title: Why are custom messages for ValueError, TypeError suppressed in argparse?
Type: enhancement Stage: resolved
Components: Extension Modules Versions: Python 3.7
process
Status: closed Resolution: not a bug
Dependencies: Superseder:
Assigned To: Nosy List: paul.j3, pgacv2
Priority: normal Keywords:

Created on 2017-05-01 16:19 by pgacv2, last changed 2018-10-18 16:40 by paul.j3. This issue is now closed.

Messages (6)
msg292669 - (view) Author: Pedro (pgacv2) Date: 2017-05-01 16:19
ArgumentParser._get_value() has two exception handlers: one for ArgumentTypeError, and one for (TypeError, ValueError). But in the latter case, any custom message I include the TypeError or ValueError is ignored and replaced with a generic "invalid value" message.

I don't see why the module wouldn't first check for a custom message in a TypeError or ValueError exception, as it does for ArgumentTypeError, and only use the generic message if a custom one is not specified. The current behavior does not let you to give the user a detailed reason for the exception unless you raise ArgumentTypeError, which is considered an implementation detail as mentioned in #20039, and also doesn't feel right for errors that are not related to the argument's type.


Code to reproduce:

import argparse

def nonnegative_not_suppressed(arg):
  try:
    x = int(arg)
    if x >= 0:
      return x
    else:
      raise argparse.ArgumentTypeError('Enter a nonnegative integer')
  except:
    raise argparse.ArgumentTypeError('Enter a nonnegative integer')

def nonnegative_suppressed(arg):
  try:
    x = int(arg)
    if x >= 0:
      return x
    else:
      raise ValueError('Enter a nonnegative integer')
  except:
    raise ValueError('Enter a nonnegative integer')

p = argparse.ArgumentParser('parser')
p.add_argument('--no-suppress', type=nonnegative_not_suppressed)
p.add_argument('--suppress', type=nonnegative_suppressed)
p.parse_args(['--no-suppress', '-4']) # Displays "Enter a nonnegative integer"
p.parse_args(['--suppress', '-4']) # Displays "invalid nonnegative_suppressed value: '-4'"
msg292673 - (view) Author: Pedro (pgacv2) Date: 2017-05-01 16:49
You can get around the message suppression by raising anything that isn't a ValueError, TypeError, or subclass thereof. It will propagate up past _get_value(). I still don't see the reason for giving special treatment to ValueError and TypeError.
msg292709 - (view) Author: paul j3 (paul.j3) * (Python triager) Date: 2017-05-02 01:55
In http://bugs.python.org/issue9353 Steven Bethard includes 'ArgumentTypeError' in a list of values that "look like they should be public but aren't in __all__::".  

I take that `__all__` list to be the ultimate authority on what's public or not, not omissions in the comments or documentation.

ArgumentTypeError is used in an illustration in the 'type' section of the documentation, https://docs.python.org/3/library/argparse.html#type

It also used in the FileType class, which I see as an example of a custom type callable.

In a function like _get_value it is far easier to test for an custom Error class than to test the message of a more generic class.

The most common `type` functions are `int` and `float`.  Are these custom or generic messages?

    ValueError: invalid literal for int() with base 10: 'one'
    ValueError: could not convert string to float: 'one'

In these common cases the ValueError is converted into a parser.Error call that identifies the argument name, the type, and the bad value, as well as correct usage.

If you issue some other error (e.g. NameError) _get_value won't catch it, and you'll get the full error stack and no 'usage'.  You'd have to wrap the 'parse_args' call in your own try/except block.

I would think than any anticipated error raised by the type callable will be related to the argument's type.  What other errors do you have in mind?  

I did not mean to say in http://bugs.python.org/issue20039 that you shouldn't use it.  The documentation could be better, but it is available.  If it works for you, use it.
msg293150 - (view) Author: Pedro (pgacv2) Date: 2017-05-06 03:02
Paul,

There are a number of scenarios in which you need to check a value and not just a type. For instance, I have a tool with several command-line arguments that need to be *nonnegative* integers, and a call to int() would not catch that. Many other utilities that ask for an argument of "how many times do you want to do X?" only make sense if you provide a nonnegative number.

Entering dates is another one. There is no callable in the datetime module or built-in that takes only one argument that can be used to validate a date. And if you write your own, you may need to make sure the date is within a specified range: only in the future, only in the past, no more/fewer than X days away from today, etc. My tool receives a date to execute a database query in the form of "select * from table where created_date >= argument_date." I can perform the validation further into the script, but performing as much validation up front when the arguments are parsed allows you to fail more quickly in the case of invalid input.

I can understand that ArgumentTypeError is available to be used when there is a problem with the arguments' type. But disallowing the use of custom messages in TypeError and ValueError seems like an arbitrary restriction with no pros and at least some cons. I assume that this was not an arbitrary decision and there really was a reason for it, but I can't see what that reason is.
msg293165 - (view) Author: paul j3 (paul.j3) * (Python triager) Date: 2017-05-06 16:32
I didn't mean to question the use of value tests in the 'type' callable.  The ArgumentTypeError example in the documentation does that kind of test.

Argparse is using the term 'type' loosely.  It just means any kind of conversion and/or test that can be applied to a string.

I doubt if _get_value() ever traps a TypeError.  That error complains about the type of the input argument. As in, for example 'int([])'.  But 'int(astring)' only produces a ValueError.  If the callable issues a TypeError when given a string, it shouldn't be used as a 'type' parameter.

If your prewritten test produces a ValueError, and you don't like the standardization, you can just wrap it in simple function reraises the 'custom' message as an ArgumentTypeError.  There's no other way of identifying a 'custom' message.
msg327985 - (view) Author: paul j3 (paul.j3) * (Python triager) Date: 2018-10-18 16:40
I'm going to close this.

Python makes it easy to test for Exception class.  Testing exception messages is messy, especially if the test wants to compare the message against 'generic message'.

I don't see anything generic about the messages produced by `int('1.23')` and `float('xxx')`.

Argparse provides a custom class, ArgumentTypeError, for use when you want to pass a custom message.  Let's leave it at that, and not try to make things more complicated.

If there's to be any change it should be in the documentation as suggested in https://bugs.python.org/issue20039.
History
Date User Action Args
2018-10-18 16:40:22paul.j3setstatus: open -> closed
resolution: not a bug
messages: + msg327985

stage: test needed -> resolved
2017-05-06 16:32:33paul.j3setmessages: + msg293165
2017-05-06 03:03:00pgacv2setmessages: + msg293150
2017-05-05 22:06:10terry.reedysettitle: Why are the custom messages for ValueError and TypeError suppressed in argparse? -> Why are custom messages for ValueError, TypeError suppressed in argparse?
stage: test needed
versions: + Python 3.7, - Python 3.5
2017-05-02 01:55:26paul.j3setnosy: + paul.j3
messages: + msg292709
2017-05-01 16:49:09pgacv2setmessages: + msg292673
2017-05-01 16:19:51pgacv2create