Created on 2013-08-28 02:42 by nikratio, last changed 2017-11-11 09:03 by chris.jerdonek.
Consider this:
$ python3 test_exc.py
Traceback (most recent call last):
File "test_exc.py", line 14, in <module>
fail1()
File "test_exc.py", line 11, in fail1
fail2()
File "test_exc.py", line 5, in fail2
raise RuntimeError('Third') from None
RuntimeError: Third
$ cat test_exc.py
def fail2():
try:
raise RuntimeError('Second')
except RuntimeError:
raise RuntimeError('Third') from None
def fail1():
try:
raise RuntimeError('First')
except:
fail2()
raise
fail1()
Any exception raised in fail2() is the immediate consequence of the 'First' exception should thus be chained to the 'First' exception.
However, if somewhere in the call stack under fail2() an exception is caught and re-raised from None (to convert between exception types), this also results in a loss of the chain to the initial exception.
The correct stacktrace (in my opinion) would be:
Traceback (most recent call last):
File "test_exc.py", line 9, in fail1
raise RuntimeError('First')
RuntimeError: First
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "test_exc.py", line 14, in <module>
fail1()
File "test_exc.py", line 11, in fail1
fail2()
File "test_exc.py", line 5, in fail2
raise RuntimeError('Third')
RuntimeError: Third
The second paragraph should of course read "...consequence of the 'First' exception *and* should thus be chained..."
*ping*
No one any comments on this at all?
raise ... from None explictly silences the printing of exception context, so I don't see the problem.
Nothing is lost either way, it just isn't displayed by default.
While I can see the appeal of treating an "outer" context differently from an "inner" one, I don't see any way for the exception context suppression machinery to tell the difference.
The traceback *display* machinery might be able to figure it out by looking at the traceback details, but such a search could be quite expensive.
Benjamin: I think that in most cases the intention of a ".. from None" is to disable printing of a stack trace for a specific exception when it occurs in the try .. except block. (In the example, that would be suppressing the stacktrace for the "Second" exception, because it is explicity converted to "Third"). I do not think that this ought to effect the printing of exceptions that occured previously and higher-up in the call chain.
In other words, I think it is natural to expect that
def foo():
try:
do_stuff
except Something:
raise SomethingElse from None
disables printing the stack trace for `Something` exceptions when they occur in the try..except block -- but not to hide the exception context for exceptions that occured before foo() was even called.
I actually think Nikratio is right about the way this *should* work (intuitively).
I'm just not sure it's feasible to *implement* those semantics in CPython without significant changes to the way exception handling works - I don't believe the exception chaining code can currently tell the difference between the cases:
# No context on Exception3 is exactly what we want
try:
try:
raise Exception1
except Exception1:
raise Exception2
except Exception2 as exc
raise Exception3 from Exception2
# Not seeing Exception1 as the context for Exception3 is surprising!
try:
raise Exception1
except Exception1:
try:
raise Exception2
except Exception2 as exc
raise Exception3 from Exception2
In a certain sense, exceptions need to be able to have *multiple* contexts to handle this case properly without losing data. Frames would need to tag exceptions appropriately with the context details as an unhandled exception passed through a frame that was currently running an exception handler.
So even though it doesn't require new syntax, I think it *does* require a PEP if we're going to change this (and we still haven't fully dealt with the consequence of the last one - the display options for tracebacks are still a bit limited)...)
Unrelated to my previous comment, I'm also wondering if this may actually represent a behavioural difference between contextlib.ExitStack and the interpreter's own exception handling machinery.
ExitStack uses a recursive->iterative transformation for its stack unwinding (see issue 14963), and it needs to do a bit of fiddling to get the context right (see issue 19092).
While the contextlib test suite goes to great lengths to try to ensure the semantics of normal stack unwinding are preserved, that definitely doesn't currently cover this case, and I'm thinking the way it works may actually be more like the behaviour Nikolaus expected in the original post (i.e. setting the context as the stack is unwound rather than when the replacement exception is raised)..
The first thing to do is to carefully specificy what the behavior should be.
2013/11/10 Nikolaus Rath <report@bugs.python.org>:
>
> Nikolaus Rath added the comment:
>
>.
>
> ----------
>
> _______________________________________
> Python tracker <report@bugs.python.org>
> <>
> _______________________________________
Yes,).
So, I've been pondering the idea of traceback/frame annotations and exception trees a bit.
And what I'm wondering is if it may make sense to have the ability to annotate *frames* at runtime, and these annotations can be qualified by module names. So, for example, you might be able to write things like:
sys.annotate_frame("codecs", "encoding", the_encoding)
sys.annotate_frame("codecs", "decoding", the_encoding)
sys.annotate_frame("traceback", "hide", True)
sys.annotate_frame("traceback", "context", exc)
And then the traceback display machinery would be updated to do something useful with the annotations.
I'm not sure how ExitStack would cope with that (or other code that fakes tracebacks) but it's something to ponder.
Walter suggested annotating the exceptions directly might work:
However, that potentially runs into nesting problems (e.g. the idna codec invokes the ascii codec), although Walter suggested a simpler mechanism that just involved appending to a list.
Adjusting the target version, since it isn't feasible to move away from the current behaviour any earlier than Python 3.5.
Something else that this might make simpler: the unittest.TestCase.subTest API is currently kind of ugly. If the subtest details could be stored as a frame annotation instead...
Actually, this won't the subTest example because that actually *suppresses* the errors, and reports them later. Annotations only help when the exception is allowed to escape.