12
12
import ssl
13
13
import struct
14
14
import urllib .parse
15
- from typing import Iterable , List , Optional , Union
15
+ from typing import Iterable , List , NoReturn , Optional , Union
16
16
17
17
import outcome
18
18
import trio
@@ -192,10 +192,29 @@ async def _close_connection(connection: WebSocketConnection) -> None:
192
192
except trio .TooSlowError :
193
193
raise DisconnectionTimeout from None
194
194
195
+ def _raise (exc : BaseException ) -> NoReturn :
196
+ __tracebackhide__ = True
197
+ context = exc .__context__
198
+ try :
199
+ raise exc
200
+ finally :
201
+ exc .__context__ = context
202
+ del exc , context
203
+
195
204
connection : WebSocketConnection | None = None
196
205
close_result : outcome .Maybe [None ] | None = None
197
206
user_error = None
198
207
208
+ # Unwrapping exception groups has a lot of pitfalls, one of them stemming from
209
+ # the exception we raise also being inside the group that's set as the context.
210
+ # This leads to loss of info unless properly handled.
211
+ # See https://github.com/python-trio/flake8-async/issues/298
212
+ # We therefore save the exception before raising it, and save our intended context,
213
+ # so they can be modified in the `finally`.
214
+ exc_to_raise = None
215
+ exc_context = None
216
+ # by avoiding use of `raise .. from ..` we leave the original __cause__
217
+
199
218
try :
200
219
async with trio .open_nursery () as new_nursery :
201
220
result = await outcome .acapture (_open_connection , new_nursery )
@@ -216,7 +235,7 @@ async def _close_connection(connection: WebSocketConnection) -> None:
216
235
except _TRIO_EXC_GROUP_TYPE as e :
217
236
# user_error, or exception bubbling up from _reader_task
218
237
if len (e .exceptions ) == 1 :
219
- raise copy_exc (e .exceptions [0 ]) from e . exceptions [ 0 ]. __cause__
238
+ _raise (e .exceptions [0 ])
220
239
221
240
# contains at most 1 non-cancelled exceptions
222
241
exception_to_raise : BaseException | None = None
@@ -229,25 +248,40 @@ async def _close_connection(connection: WebSocketConnection) -> None:
229
248
else :
230
249
if exception_to_raise is None :
231
250
# all exceptions are cancelled
232
- # prefer raising the one from the user, for traceback reasons
251
+ # we reraise the user exception and throw out internal
233
252
if user_error is not None :
234
- # no reason to raise from e, just to include a bunch of extra
235
- # cancelleds.
236
- raise copy_exc (user_error ) from user_error .__cause__
253
+ _raise (user_error )
237
254
# multiple internal Cancelled is not possible afaik
238
- raise copy_exc (e .exceptions [0 ]) from e # pragma: no cover
239
- raise copy_exc (exception_to_raise ) from exception_to_raise .__cause__
255
+ # but if so we just raise one of them
256
+ _raise (e .exceptions [0 ])
257
+ # raise the non-cancelled exception
258
+ _raise (exception_to_raise )
240
259
241
- # if we have any KeyboardInterrupt in the group, make sure to raise it.
260
+ # if we have any KeyboardInterrupt in the group, raise a new KeyboardInterrupt
261
+ # with the group as cause & context
242
262
for sub_exc in e .exceptions :
243
263
if isinstance (sub_exc , KeyboardInterrupt ):
244
- raise copy_exc ( sub_exc ) from e
264
+ raise KeyboardInterrupt from e
245
265
246
266
# Both user code and internal code raised non-cancelled exceptions.
247
- # We "hide" the internal exception(s) in the __cause__ and surface
248
- # the user_error.
267
+ # We set the context to be an exception group containing internal exceptions
268
+ # and, if not None, ` user_error.__context__`
249
269
if user_error is not None :
250
- raise copy_exc (user_error ) from e
270
+ exceptions = [subexc for subexc in e .exceptions if subexc is not user_error ]
271
+ eg_substr = ''
272
+ # there's technically loss of info here, with __suppress_context__=True you
273
+ # still have original __context__ available, just not printed. But we delete
274
+ # it completely because we can't partially suppress the group
275
+ if user_error .__context__ is not None and not user_error .__suppress_context__ :
276
+ exceptions .append (user_error .__context__ )
277
+ eg_substr = ' and the context for the user exception'
278
+ eg_str = (
279
+ "Both internal and user exceptions encountered. This group contains "
280
+ "the internal exception(s)" + eg_substr + "."
281
+ )
282
+ user_error .__context__ = BaseExceptionGroup (eg_str , exceptions )
283
+ user_error .__suppress_context__ = False
284
+ _raise (user_error )
251
285
252
286
raise TrioWebsocketInternalError (
253
287
"The trio-websocket API is not expected to raise multiple exceptions. "
0 commit comments