@@ -1070,12 +1070,19 @@ async def child2() -> None:
1070
1070
]
1071
1071
1072
1072
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).
1076
1079
#
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 :
1079
1086
child_task : _core .Task | None = None
1080
1087
1081
1088
async def child () -> None :
@@ -1084,21 +1091,28 @@ async def child() -> None:
1084
1091
1085
1092
try :
1086
1093
raise KeyError
1087
- except Exception :
1088
- with suppress (Exception ):
1094
+ except KeyError :
1095
+ with suppress (ValueError ):
1089
1096
await sleep_forever ()
1090
1097
raise
1091
1098
1092
- with pytest .raises (KeyError ):
1099
+ with pytest .raises (KeyError ) as excinfo :
1093
1100
async with _core .open_nursery () as nursery :
1094
1101
nursery .start_soon (child )
1095
1102
await wait_all_tasks_blocked ()
1096
1103
_core .reschedule (not_none (child_task ), outcome .Error (ValueError ()))
1097
1104
1105
+ assert excinfo .value .__context__ is None
1106
+
1098
1107
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 :
1102
1116
child_task : _core .Task | None = None
1103
1117
1104
1118
async def child () -> None :
@@ -1107,7 +1121,7 @@ async def child() -> None:
1107
1121
1108
1122
try :
1109
1123
raise KeyError
1110
- except Exception :
1124
+ except KeyError :
1111
1125
await sleep_forever ()
1112
1126
1113
1127
with pytest .raises (ValueError ) as excinfo :
@@ -1119,6 +1133,71 @@ async def child() -> None:
1119
1133
assert isinstance (excinfo .value .__context__ , KeyError )
1120
1134
1121
1135
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
+
1122
1201
async def test_nursery_exception_chaining_doesnt_make_context_loops () -> None :
1123
1202
async def crasher () -> NoReturn :
1124
1203
raise KeyError
0 commit comments