Skip to content

Raise the "original" exception from Interpreter.run()? #43

Open
@ericsnowcurrently

Description

@ericsnowcurrently

(Note that this is more of a future enhancement than a blocker for PEP 554, etc.)

from PEP 554:

Regarding uncaught exceptions in Interpreter.run(), we noted that they are "effectively" propagated into the code where run() was called. To prevent leaking exceptions (and tracebacks) between interpreters, we create a surrogate of the exception and its traceback (see traceback.TracebackException), set it to __cause__ on a new RunFailedError, and raise that.

Raising (a proxy of) the exception directly is problematic since it's harder to distinguish between an error in the run() call and an uncaught exception from the subinterpreter.


Currently uncaught exceptions from Interpreter.run() are stringified and then wrapped in a RunFailedError before being raised in the calling interpreter. With that you can always know if an uncaught exception came from a subinterpreter. On top of that, in #17 we're addressing the issue of lost information for the propagated exception (e.g. type, attrs, traceback, __cause__). Once that is resolved the boundary between the two interpreters will be clear and the traceback informative about the course of events.

At that point, however, you still always have to deal with RunFailedError if you otherwise don't care that the exception came from another interpreter (which I expect most folks won't). This leads to annoying boilerplate code. In #17 the example demonstrates some of the clunkiness.

We can address this, but must remember there's sometimes value to knowing that an exception came out of another interpreter. So we shouldn't just throw that information away either. That said, there's perhaps even more value to not requiring boilerplate code.

Here are some options for handling the original exception more easily:

A. raise the propagated exception directly in the calling interpreter
B. like A, but set __cause__ on the exception to the RunFailedError
C. like B, but set __propagated__ instead
D. raise RunFailedError and re-raise original via RunFailedError.raise() (or .reraise())
E. provide a context manager that re-raises the "original" exception

Contrast the example in #17 with the alternatives presented above:

A:

>>> interp.run(script)
Traceback (most recent call last):
  File "<stdin>", line 3
    raise SpamError('eggs')
  File "<stdin>", line 7
    interp.run(script)
SpamError: eggs

You don't have to treat the unhandled exception specially, but you lose context. Perhaps you could get it back by walking the traceback looking for the point that interp.run() is called (there could even be a helper for that). However, at the very least the traceback won't be nearly as useful.

B:

>>> interp.run(script)
Traceback (most recent call last):
  File "<stdin>", line 3
    raise SpamError('eggs')
SpamError: eggs

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 7
    interp.run(script)
RunFailedError: SpamError: eggs

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 3
    raise SpamError('eggs')
SpamError: eggs
>>> try:
...     interp.run(script)  # or anywhere else up the call chain...
... except Exception as exc:
...     if exc.__cause__ is None or type(exc.__cause__) is not RunFailedError:
...         raise
...     # handle it

Then you get the best of both worlds (at the expense of an inversion of the exception chain):

C:

Like B, but doesn't reverse the meaning of __cause__. However, either the traceback machinery would have to know how to deal with __propagated__ or we lose the information.

D:

...

E:

...

Metadata

Metadata

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions