classification
Title: Generator/coroutine 'throw' discards exc_info state, which is bad
Type: behavior Stage:
Components: Versions: Python 3.7, Python 3.6, Python 3.5
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: martin.panter, njs
Priority: normal Keywords:

Created on 2017-02-17 11:01 by njs, last changed 2017-02-17 11:49 by martin.panter.

Messages (2)
msg287987 - (view) Author: Nathaniel Smith (njs) * Date: 2017-02-17 11:01
Example 1:

-----------
def f():
    try:
        raise KeyError
    except Exception:
        yield

gen = f()
gen.send(None)
gen.throw(ValueError)
---------

Output:
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in f
ValueError

Expected output:

Something involving the string "During handling of the above exception, another exception occurred", and a traceback for the original KeyError.


Example 2:

-----------
import sys

def f():
    try:
        raise KeyError
    except Exception:
        print(sys.exc_info())
        try:
            yield
        except Exception:
            pass
        print(sys.exc_info())
        raise

gen = f()
gen.send(None)
gen.throw(ValueError)
-----------

Output:

(<class 'KeyError'>, KeyError(), <traceback object at 0x7f67ce3c3f88>)
(None, None, None)
Traceback (most recent call last):
  File "/tmp/foo.py", line 17, in <module>
    gen.throw(ValueError)
  File "/tmp/foo.py", line 13, in f
    raise
RuntimeError: No active exception to reraise

Expected output: certainly not that :-)


This seems to happen because normally, generators save the current exc_info when yielding, and then restore it when re-entered. But, if we re-enter through 'throw' (throwflag is true), this is disabled:

https://github.com/python/cpython/blob/b2ee40ed9c9041dcff9c898aa19aacf9ec60308a/Python/ceval.c#L1027

This check seems to have been added in ae5f2f4a39e6a3f4c45e9dc95bd4e1fe5dfb60f2 as a fix for:

https://bugs.python.org/issue7173

which had to do with a nasty situation involving a generator object that  was part of a reference cycle: the gc sometimes would free the objects stored in the generator's saved exc_info, and then try to clean up the generator by throwing in a GeneratorExit.

AFAICT this situation shouldn't be possible anymore thanks to PEP 442, which makes it so that finalizers are run before any part of the cycle is freed. And in any case it certainly doesn't justify breaking code like the examples above.

(Note: the examples use generators for simplicity, but of course the way I noticed this was that I had some async/await code where exceptions were mysteriously disappearing instead of showing up in __context__ and couldn't figure out why. It's likely that more people will run into this in the future as async/await becomes more widely used. As a workaround for now I'll probably modify my coroutine runner so that it never uses 'throw'.)
msg287992 - (view) Author: Martin Panter (martin.panter) * (Python committer) Date: 2017-02-17 11:49
The second example seems like the original complaint in Issue 25612. That spawned a separate bug related to the first situation, which was later closed: Issue 25683.
History
Date User Action Args
2017-02-17 11:49:20martin.pantersettype: behavior

messages: + msg287992
nosy: + martin.panter
2017-02-17 11:01:49njscreate