classification
Title: Provide a more convenient way to set an exception as "active", from Python code
Type: enhancement Stage:
Components: Versions:
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: Mark.Shannon, gvanrossum, iritkatriel, ncoghlan, njs, yselivanov
Priority: normal Keywords:

Created on 2021-10-04 06:42 by njs, last changed 2021-10-10 05:47 by gvanrossum.

Messages (4)
msg403121 - (view) Author: Nathaniel Smith (njs) * (Python committer) Date: 2021-10-04 06:42
Inside 'except' and 'finally' blocks, the interpreter keeps track of the 'active exception in the thread-state. It can be introspected via `sys.exc_info()`, it enables bare `raise`, and it triggers implicit context propagation.

In an odd bit of synchronicity, I recently bumped into two completely separate cases where Python code would benefit from being able to control this state directly:

- in the flat exception groups discussion [1], I noted that it would be useful if concurrency libraries could propagate this state into child tasks

- in bpo-27089, regarding some gnarly code in contextlib.ExitStack that simulates a set of nested try/finally blocks by running callback with the wrong exception context, and then introspecting the result and trying to fix it up after the fact. It turns out this code is difficult to understand and subtly buggy -- the PR at gh-27089 fixes one of the bugs here, but there are others [2]. It would be much simpler if it could just set the correct `exc_info` before calling each cleanup function, and then no fixups would be needed things would be set up correctly in the first place.

[1] https://discuss.python.org/t/flat-exception-groups-alternative-to-pep-654/10433

[2] https://github.com/python/cpython/pull/27089#issuecomment-932892687

But, currently, the *only* way to make an exception 'active' like this is to raise it and then catch it again. And this has some significant limitations:

- It unconditionally mutates the exception -- in particular both __context__ and __traceback__ are modified

- The "active exception" has type Optional[BaseException], since try/raise/except is block syntax, and you can't `raise None`. So if you want to propagate an arbitrary `sys.exc_info()` into a child task or into a simulated `finally` block, then you need two separate code paths depending on whether `sys.exc_info()[1]` is None or not.

- technically I think you can work around both of these issues with enough effort... but since try/raise/except is block syntax, the workarounds can't be hidden inside a function; you have to inline them into each usage site.

So... I'm thinking maybe we should have some stupid-simple, sharp-edged API to set `sys.exc_info()` from Python. Like, idk, `sys.set_exc_info(...)`, as a trivial wrapper around `PyErr_Restore`.

My main uncertainty is that I know the code for handling the exception state is quite complex, between the "exception stack", the tricky optimizations around partially-initialized exc_info tuples, pushing/popping the exc state in ceval.c, the tricks generators/coroutines use to save/restore exc_info across yields, etc. There might be some hidden dragons in here that require careful handling, and maybe it'll turn out we need to push/pop an exception instead of just setting it, or something like that.

But does the core idea make sense? Anyone aware of any dragons off the top of your head?
msg403122 - (view) Author: Nathaniel Smith (njs) * (Python committer) Date: 2021-10-04 06:43
Ugh, the gnarly ExitStack bug is bpo-44594, not whatever I wrote above
msg403287 - (view) Author: Guido van Rossum (gvanrossum) * (Python committer) Date: 2021-10-06 05:12
Do you think the API you're looking for is available at the C level? Like PyErr_SetExcInfo()?
msg403572 - (view) Author: Guido van Rossum (gvanrossum) * (Python committer) Date: 2021-10-10 05:47
In a brief off-line chat we concluded that the desired API would be closer to PyErr_Restore().
History
Date User Action Args
2021-10-10 05:47:09gvanrossumsetmessages: + msg403572
2021-10-06 05:12:13gvanrossumsetmessages: + msg403287
2021-10-04 06:43:54njssetmessages: + msg403122
2021-10-04 06:42:56njscreate