Skip to content

Commit 783093f

Browse files
authored
Merge pull request #2879 from gschaffner/cpython-throw-bugs-testing
Test that Trio works around some CPython `.throw` bugs present in >= 3.9
2 parents c7d8a93 + 6784759 commit 783093f

File tree

2 files changed

+94
-13
lines changed

2 files changed

+94
-13
lines changed

src/trio/_core/_run.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2546,9 +2546,11 @@ def unrolled_run(
25462546
try:
25472547
# We used to unwrap the Outcome object here and send/throw
25482548
# its contents in directly, but it turns out that .throw()
2549-
# is buggy, at least before CPython 3.9:
2549+
# is buggy on CPython (all versions at time of writing):
25502550
# https://bugs.python.org/issue29587
25512551
# https://bugs.python.org/issue29590
2552+
# https://bugs.python.org/issue40694
2553+
# https://github.com/python/cpython/issues/108668
25522554
# So now we send in the Outcome object and unwrap it on the
25532555
# other side.
25542556
msg = task.context.run(next_send_fn, next_send)

src/trio/_core/_tests/test_run.py

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,12 +1070,19 @@ async def child2() -> None:
10701070
]
10711071

10721072

1073-
# Before CPython 3.9, using .throw() to raise an exception inside a
1074-
# coroutine/generator causes the original exc_info state to be lost, so things
1075-
# like re-raising and exception chaining are broken.
1073+
# On all CPython versions (at time of writing), using .throw() to raise an
1074+
# exception inside a coroutine/generator can cause the original `exc_info` state
1075+
# to be lost, so things like re-raising and exception chaining are broken unless
1076+
# Trio implements its workaround. At time of writing, CPython main (3.13-dev)
1077+
# and every CPython release (excluding releases for old Python versions not
1078+
# supported by Trio) is affected (albeit in differing ways).
10761079
#
1077-
# https://bugs.python.org/issue29587
1078-
async def test_exc_info_after_yield_error() -> None:
1080+
# If the `ValueError()` gets sent in via `throw` and is suppressed, then CPython
1081+
# loses track of the original `exc_info`:
1082+
# https://bugs.python.org/issue25612 (Example 1)
1083+
# https://bugs.python.org/issue29587 (Example 2)
1084+
# This is fixed in CPython >= 3.7.
1085+
async def test_exc_info_after_throw_suppressed() -> None:
10791086
child_task: _core.Task | None = None
10801087

10811088
async def child() -> None:
@@ -1084,21 +1091,28 @@ async def child() -> None:
10841091

10851092
try:
10861093
raise KeyError
1087-
except Exception:
1088-
with suppress(Exception):
1094+
except KeyError:
1095+
with suppress(ValueError):
10891096
await sleep_forever()
10901097
raise
10911098

1092-
with pytest.raises(KeyError):
1099+
with pytest.raises(KeyError) as excinfo:
10931100
async with _core.open_nursery() as nursery:
10941101
nursery.start_soon(child)
10951102
await wait_all_tasks_blocked()
10961103
_core.reschedule(not_none(child_task), outcome.Error(ValueError()))
10971104

1105+
assert excinfo.value.__context__ is None
1106+
10981107

1099-
# Similar to previous test -- if the ValueError() gets sent in via 'throw',
1100-
# then Python's normal implicit chaining stuff is broken.
1101-
async def test_exception_chaining_after_yield_error() -> None:
1108+
# Similar to previous test -- if the `ValueError()` gets sent in via 'throw' and
1109+
# propagates out, then CPython doesn't set its `__context__` so normal implicit
1110+
# exception chaining is broken:
1111+
# https://bugs.python.org/issue25612 (Example 2)
1112+
# https://bugs.python.org/issue25683
1113+
# https://bugs.python.org/issue29587 (Example 1)
1114+
# This is fixed in CPython >= 3.9.
1115+
async def test_exception_chaining_after_throw() -> None:
11021116
child_task: _core.Task | None = None
11031117

11041118
async def child() -> None:
@@ -1107,7 +1121,7 @@ async def child() -> None:
11071121

11081122
try:
11091123
raise KeyError
1110-
except Exception:
1124+
except KeyError:
11111125
await sleep_forever()
11121126

11131127
with pytest.raises(ValueError) as excinfo:
@@ -1119,6 +1133,71 @@ async def child() -> None:
11191133
assert isinstance(excinfo.value.__context__, KeyError)
11201134

11211135

1136+
# Similar to previous tests -- if the `ValueError()` gets sent into an inner
1137+
# `await` via 'throw' and is suppressed there, then CPython loses track of
1138+
# `exc_info` in the inner coroutine:
1139+
# https://github.com/python/cpython/issues/108668
1140+
# This is unfixed in CPython at time of writing.
1141+
async def test_exc_info_after_throw_to_inner_suppressed() -> None:
1142+
child_task: _core.Task | None = None
1143+
1144+
async def child() -> None:
1145+
nonlocal child_task
1146+
child_task = _core.current_task()
1147+
1148+
try:
1149+
raise KeyError
1150+
except KeyError as exc:
1151+
await inner(exc)
1152+
raise
1153+
1154+
async def inner(exc: BaseException) -> None:
1155+
with suppress(ValueError):
1156+
await sleep_forever()
1157+
assert not_none(sys.exc_info()[1]) is exc
1158+
1159+
with pytest.raises(KeyError) as excinfo:
1160+
async with _core.open_nursery() as nursery:
1161+
nursery.start_soon(child)
1162+
await wait_all_tasks_blocked()
1163+
_core.reschedule(not_none(child_task), outcome.Error(ValueError()))
1164+
1165+
assert excinfo.value.__context__ is None
1166+
1167+
1168+
# Similar to previous tests -- if the `ValueError()` gets sent into an inner
1169+
# `await` via `throw` and propagates out, then CPython incorrectly sets its
1170+
# `__context__` so normal implicit exception chaining is broken:
1171+
# https://bugs.python.org/issue40694
1172+
# This is unfixed in CPython at time of writing.
1173+
async def test_exception_chaining_after_throw_to_inner() -> None:
1174+
child_task: _core.Task | None = None
1175+
1176+
async def child() -> None:
1177+
nonlocal child_task
1178+
child_task = _core.current_task()
1179+
1180+
try:
1181+
raise KeyError
1182+
except KeyError:
1183+
await inner()
1184+
1185+
async def inner() -> None:
1186+
try:
1187+
raise IndexError
1188+
except IndexError:
1189+
await sleep_forever()
1190+
1191+
with pytest.raises(ValueError) as excinfo:
1192+
async with _core.open_nursery() as nursery:
1193+
nursery.start_soon(child)
1194+
await wait_all_tasks_blocked()
1195+
_core.reschedule(not_none(child_task), outcome.Error(ValueError()))
1196+
1197+
assert isinstance(excinfo.value.__context__, IndexError)
1198+
assert isinstance(excinfo.value.__context__.__context__, KeyError)
1199+
1200+
11221201
async def test_nursery_exception_chaining_doesnt_make_context_loops() -> None:
11231202
async def crasher() -> NoReturn:
11241203
raise KeyError

0 commit comments

Comments
 (0)