-
-
Notifications
You must be signed in to change notification settings - Fork 11k
ENH: Fix exception causes in _iotools.py #15731
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Better error messages are a good project/improvement, although I am wondering if most of these should not rather use |
I'm strongly against using Also, some tools like Django and Sentry show you all the local variables for your stacktraces, which is a godsend. These often have important information that sheds light on what went wrong, and if you remove the traceback they'll be gone. |
In a community discussion we would prefer to go through these one at a time and add |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the pull request, @cool-RR. I added comments in-line. Based on my attempts to actually trigger these exceptions, I suggested that we not use from e
in one case. In another case, I have a question about how to actually trigger the exception. It would be nice to be able to exercise these changes before committing them. For the remaining changes, using from e
is probably OK.
numpy/lib/_iotools.py
Outdated
# dtype_or_func must be a function, then | ||
if not hasattr(dtype_or_func, '__call__'): | ||
errmsg = ("The input argument `dtype` is neither a" | ||
" function nor a dtype (got '%s' instead)") | ||
raise TypeError(errmsg % type(dtype_or_func)) | ||
raise TypeError(errmsg % type(dtype_or_func)) from e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't use from e
here. The original exception doesn't provide useful information. For example,
In [29]: conv = StringConverter("foo")
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
~/mc37np/lib/python3.7/site-packages/numpy-1.19.0.dev0+116a021-py3.7-macosx-10.9-x86_64.egg/numpy/lib/_iotools.py in __init__(self, dtype_or_func, default, missing_values, locked)
606 self.func = None
--> 607 dtype = np.dtype(dtype_or_func)
608 except TypeError as e:
TypeError: data type 'foo' not understood
The above exception was the direct cause of the following exception:
TypeError Traceback (most recent call last)
<ipython-input-29-f4dc02a5945d> in <module>
----> 1 conv = StringConverter("foo")
~/mc37np/lib/python3.7/site-packages/numpy-1.19.0.dev0+116a021-py3.7-macosx-10.9-x86_64.egg/numpy/lib/_iotools.py in __init__(self, dtype_or_func, default, missing_values, locked)
611 errmsg = ("The input argument `dtype` is neither a"
612 " function nor a dtype (got '%s' instead)")
--> 613 r
10000
aise TypeError(errmsg % type(dtype_or_func)) from e
614 # Set the function
615 self.func = dtype_or_func
TypeError: The input argument `dtype` is neither a function nor a dtype (got '<class 'str'>' instead)
The first exception, TypeError: data type 'foo' not understood
doesn't provide any information that is not also in the final exception message (TypeError: The input argument
dtype is neither a function nor a dtype (got '<class 'str'>' instead)
, so it is noise. We should use from None
here and not expose the first exception.
numpy/lib/_iotools.py
Outdated
except OverflowError: | ||
raise ValueError | ||
except OverflowError as e: | ||
raise ValueError from e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have an example that triggers this exception? If this exception is raised, it is then caught a few lines down and raised again, so the end result of the use of from e
in all these try-except
statements is the user being given three chained exceptions. That seems pretty noisy, and probably not useful.
if value.strip() in self.missing_values: | ||
if not self._status: | ||
self._checked = False | ||
return self.default | ||
raise ValueError("Cannot convert string '%s'" % value) | ||
raise ValueError("Cannot convert string '%s'" % value) from e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like the previous exception in the will give include the details of what went wrong. This one just says, in effect "fail!". So I guess this use of from e
is OK.
(I'm starting to think that the way this code uses exceptions is awkward, and could use a redesign, but that will have to wait for another time.)
# Raise an exception if we locked the converter... | ||
if self._locked: | ||
errmsg = "Converter is locked and cannot be upgraded" | ||
raise ConverterLockError(errmsg) | ||
raise ConverterLockError(errmsg) from e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The exception chain is a bit noisy, but it looks like the early exceptions have useful information, so I guess this use of from e
is OK:
In [121]: conv = StringConverter(int, locked=True)
In [122]: conv.upgrade('0.')
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
~/mc37np/lib/python3.7/site-packages/numpy-1.19.0.dev0+116a021-py3.7-macosx-10.9-x86_64.egg/numpy/lib/_iotools.py in _strict_call(self, value)
687 # We check if we can convert the value using the current function
--> 688 new_value = self.func(value)
689
ValueError: invalid literal for int() with base 10: '0.'
The above exception was the direct cause of the following exception:
ValueError Traceback (most recent call last)
~/mc37np/lib/python3.7/site-packages/numpy-1.19.0.dev0+116a021-py3.7-macosx-10.9-x86_64.egg/numpy/lib/_iotools.py in upgrade(self, value)
736 try:
--> 737 return self._strict_call(value)
738 except ValueError as e:
~/mc37np/lib/python3.7/site-packages/numpy-1.19.0.dev0+116a021-py3.7-macosx-10.9-x86_64.egg/numpy/lib/_iotools.py in _strict_call(self, value)
706 return self.default
--> 707 raise ValueError("Cannot convert string '%s'" % value) from e
708 #
ValueError: Cannot convert string '0.'
The above exception was the direct cause of the following exception:
ConverterLockError Traceback (most recent call last)
<ipython-input-122-b82e0b39004c> in <module>
----> 1 conv.upgrade('0.')
~/mc37np/lib/python3.7/site-packages/numpy-1.19.0.dev0+116a021-py3.7-macosx-10.9-x86_64.egg/numpy/lib/_iotools.py in upgrade(self, value)
740 if self._locked:
741 errmsg = "Converter is locked and cannot be upgraded"
--> 742 raise ConverterLockError(errmsg) from e
743 _statusmax = len(self._mapper)
744 # Complains if we try to upgrade by the maximum
ConverterLockError: Converter is locked and cannot be upgraded
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is correct.
This makes the error message The above exception was the direct cause of the following exception:
.
However, that's not what's happening here. What's happening here is that something else went wrong while we tried to recover (by upgrading the data type). Today, the message is
During handling of the above exception, another exception occurred:
This is a more accurate message. So this line was better unchanged.
_statusmax = len(self._mapper) | ||
# Complains if we try to upgrade by the maximum | ||
_status = self._status | ||
if _status == _statusmax: | ||
errmsg = "Could not find a valid conversion function" | ||
raise ConverterError(errmsg) | ||
raise ConverterError(errmsg) from e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like it is in the same situation as the previous one: it makes a noisy exception, but there might be useful info. in there, so OK.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above
# Raise an exception if we locked the converter... | ||
if self._locked: | ||
errmsg = "Converter is locked and cannot be upgraded" | ||
raise ConverterLockError(errmsg) | ||
raise ConverterLockError(errmsg) from e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto, so OK.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above
_statusmax = len(self._mapper) | ||
# Complains if we try to upgrade by the maximum | ||
_status = self._status | ||
if _status == _statusmax: | ||
raise ConverterError( | ||
"Could not find a valid conversion function" | ||
) | ||
) from e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto, so OK.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above
116a021
to
536afeb
Compare
@WarrenWeckesser Thanks for your review. That was very thorough. I disagree with the decision to use |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think any of the ConversionError
cases make sense to chain exc
685C
eption __cause__
s, these look like __context__
chains to me, which is what we already had.
# Raise an exception if we locked the converter... | ||
if self._locked: | ||
errmsg = "Converter is locked and cannot be upgraded" | ||
raise ConverterLockError(errmsg) | ||
raise ConverterLockError(errmsg) from e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is correct.
This makes the error message The above exception was the direct cause of the following exception:
.
However, that's not what's happening here. What's happening here is that something else went wrong while we tried to recover (by upgrading the data type). Today, the message is
During handling of the above exception, another exception occurred:
This is a more accurate message. So this line was better unchanged.
_statusmax = len(self._mapper) | ||
# Complains if we try to upgrade by the maximum | ||
_status = self._status | ||
if _status == _statusmax: | ||
errmsg = "Could not find a valid conversion function" | ||
raise ConverterError(errmsg) | ||
raise ConverterError(errmsg) from e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above
# Raise an exception if we locked the converter... | ||
if self._locked: | ||
errmsg = "Converter is locked and cannot be upgraded" | ||
raise ConverterLockError(errmsg) | ||
raise ConverterLockError(errmsg) from e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above
_statusmax = len(self._mapper) | ||
# Complains if we try to upgrade by the maximum | ||
_status = self._status | ||
if _status == _statusmax: | ||
raise ConverterError( | ||
"Could not find a valid conversion function" | ||
) | ||
) from e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above
Apologies for the merge conflicts. Note that to merge keeping the semantics of this patch (that I'm arguing against above) you'd need to write: except ValueError as e:
try:
self._do_upgrade()
except ConversionError as e2:
raise e2 from e1 # claim that e2 was _caused_ by e1 Again though, I'd recommend you not do this. |
@cool-RR, I've found a bunch of places elsewhere in numpy where changing to use
etc |
@eric-wieser Hmm, that's not enjoyable enough for me to do, so I'll leave that to whoever's interested. |
I recently went over Matplotlib and Pandas, fixing a small mistake in the way that Python 3's exception chaining is used. If you're interested, I can do it here too. I've done it on just one file right now.
The mistake is this: In some parts of the code, an exception is being caught and replaced with a more user-friendly error. In these cases the syntax
raise new_error from old_error
needs to be used.Python 3's exception chaining means it shows not only the traceback of the current exception, but that of the original exception (and possibly more.) This is regardless of
raise from
. The usage ofraise from
tells Python to put a more accurate message between the tracebacks. Instead of this:You'll get this:
The first is inaccurate, because it signifies a bug in the exception-handling code itself, which is a separate situation than wrapping an exception.
Let me know what you think!