Skip to content

Commit 7da2058

Browse files
authored
[mypyc] Fix tracebacks of exceptions passed to throw() (#22287)
When calling single-argument `throw(ex)` on a coroutine or generator, the `traceback` argument is `None`, so passing it to `PyErr_Restore` clears the existing exception traceback. Recover the traceback from the exception instead to match interpreted Python. Don't add a traceback entry when forwarding an exception injected through `throw()` at `await`/`yield from`. Otherwise, the child coro/gen would see the parent's frame in its traceback. The parent's entry is still added when propagating the exception, so this also avoids a duplicated entry.
1 parent e6dd0ea commit 7da2058

4 files changed

Lines changed: 94 additions & 10 deletions

File tree

‎mypyc/irbuild/statement.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1439,8 +1439,12 @@ def native_step(sent: Value) -> Value:
14391439
builder.goto_and_activate(loop_block)
14401440

14411441
def try_body() -> None:
1442+
# Traceback will be added if yield_from_except_op propagates an error.
1443+
# Skip it here to not have a duplicated entry.
14421444
builder.assign(
1443-
received_reg, emit_yield(builder, builder.read(to_yield_reg, line), line), line
1445+
received_reg,
1446+
emit_yield(builder, builder.read(to_yield_reg, line), NO_TRACEBACK_LINE_NO),
1447+
line,
14441448
)
14451449

14461450
def except_body() -> None:

‎mypyc/lib-rt/exc_ops.c‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,16 @@ void CPyErr_SetObjectAndTraceback(PyObject *type, PyObject *value, PyObject *tra
8383
type = (PyObject *)Py_TYPE(value);
8484
}
8585

86-
// Set the value and traceback of an error. Because calling
87-
// PyErr_Restore takes away a reference to each object passed in
88-
// as an argument, we manually increase the reference count of
89-
// each argument before calling it.
86+
// PyErr_Restore steals a reference to each argument.
9087
Py_INCREF(type);
9188
Py_INCREF(value);
92-
Py_INCREF(traceback);
89+
// An omitted traceback (or None) preserves an exception instance's
90+
// existing traceback, as generator.throw() and coroutine.throw() do.
91+
if (Py_IsNone(traceback) && PyExceptionInstance_Check(value)) {
92+
traceback = PyException_GetTraceback(value);
93+
} else {
94+
Py_INCREF(traceback);
95+
}
9396
PyErr_Restore(type, value, traceback);
9497
}
9598

‎mypyc/test-data/run-async.test‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2733,6 +2733,87 @@ def test_close_suspended_native_child() -> None:
27332733
coro.send(None)
27342734
coro.close()
27352735

2736+
async def throw_child(explicit: bool) -> None:
2737+
try:
2738+
await Suspend(1)
2739+
except ValueError as exc:
2740+
if explicit:
2741+
raise exc
2742+
raise
2743+
2744+
async def throw_parent(explicit: bool) -> None:
2745+
await throw_child(explicit)
2746+
2747+
def traceback_frames(tb: Any) -> list[str]:
2748+
result = []
2749+
while tb is not None:
2750+
result.append(tb.tb_frame.f_code.co_name)
2751+
tb = tb.tb_next
2752+
return result
2753+
2754+
def throw_origin() -> None:
2755+
raise ValueError('injected')
2756+
2757+
def check_throw_traceback(nested: bool, explicit: bool, kind: str) -> None:
2758+
error: Any
2759+
try:
2760+
throw_origin()
2761+
except ValueError as exc:
2762+
error = exc
2763+
if kind == 'fresh':
2764+
error = ValueError('fresh')
2765+
original = traceback_frames(error.__traceback__)
2766+
coro = (throw_parent if nested else throw_child)(explicit)
2767+
assert coro.send(None) == 0
2768+
with assertRaises(ValueError):
2769+
if kind == 'triple':
2770+
coro.throw(type(error), error, error.__traceback__)
2771+
else:
2772+
coro.throw(error)
2773+
expected = ['throw_parent'] if nested else []
2774+
if explicit:
2775+
expected += ['throw_child']
2776+
expected += ['throw_child', '__await__'] + original
2777+
# Skip assertRaises and this helper's throw() call.
2778+
actual = traceback_frames(error.__traceback__)[2:]
2779+
assert actual == expected, (nested, explicit, kind, actual, expected)
2780+
2781+
def test_throw_fresh_reraise() -> None:
2782+
check_throw_traceback(False, False, 'fresh')
2783+
2784+
def test_throw_existing_reraise() -> None:
2785+
check_throw_traceback(False, False, 'existing')
2786+
2787+
def test_throw_triple_reraise() -> None:
2788+
check_throw_traceback(False, False, 'triple')
2789+
2790+
def test_throw_fresh_raise_exc() -> None:
2791+
check_throw_traceback(False, True, 'fresh')
2792+
2793+
def test_throw_existing_raise_exc() -> None:
2794+
check_throw_traceback(False, True, 'existing')
2795+
2796+
def test_throw_triple_raise_exc() -> None:
2797+
check_throw_traceback(False, True, 'triple')
2798+
2799+
def test_throw_nested_fresh_reraise() -> None:
2800+
check_throw_traceback(True, False, 'fresh')
2801+
2802+
def test_throw_nested_existing_reraise() -> None:
2803+
check_throw_traceback(True, False, 'existing')
2804+
2805+
def test_throw_nested_triple_reraise() -> None:
2806+
check_throw_traceback(True, False, 'triple')
2807+
2808+
def test_throw_nested_fresh_raise_exc() -> None:
2809+
check_throw_traceback(True, True, 'fresh')
2810+
2811+
def test_throw_nested_existing_raise_exc() -> None:
2812+
check_throw_traceback(True, True, 'existing')
2813+
2814+
def test_throw_nested_triple_raise_exc() -> None:
2815+
check_throw_traceback(True, True, 'triple')
2816+
27362817
[typing fixtures/typing-full.pyi]
27372818

27382819
[case testRunAsyncNativeCoroutineDrivenByInterpretedCode]

‎mypyc/test-data/run-generators.test‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -410,12 +410,8 @@ with ctx_manager() as c:
410410
raise Exception
411411
File "native.py", line 10, in generator
412412
yield 3
413-
File "native.py", line 31, in wrapper
414-
return (yield from x)
415413
File "native.py", line 9, in generator
416414
yield 2
417-
File "native.py", line 31, in wrapper
418-
return (yield from x)
419415
caught exception without value
420416
caught exception with value some string
421417
caught exception with value some other string

0 commit comments

Comments
 (0)