11
11
import ssl
12
12
import struct
13
13
import urllib .parse
14
- from typing import Iterable , List , Optional , Union
14
+ from typing import Iterable , List , NoReturn , Optional , Union
15
15
16
16
import outcome
17
17
import trio
@@ -151,14 +151,14 @@ async def open_websocket(
151
151
# yield to user code. If only one of those raise a non-cancelled exception
152
152
# we will raise that non-cancelled exception.
153
153
# If we get multiple cancelled, we raise the user's cancelled.
154
- # If both raise exceptions, we raise the user code's exception with the entire
155
- # exception group as the __cause__.
154
+ # If both raise exceptions, we raise the user code's exception with __context__
155
+ # set to a group containing internal exception(s) + any user exception __context__
156
156
# If we somehow get multiple exceptions, but no user exception, then we raise
157
157
# TrioWebsocketInternalError.
158
158
159
159
# If closing the connection fails, then that will be raised as the top
160
160
# exception in the last `finally`. If we encountered exceptions in user code
161
- # or in reader task then they will be set as the `__cause__ `.
161
+ # or in reader task then they will be set as the `__context__ `.
162
162
163
163
164
164
async def _open_connection (nursery : trio .Nursery ) -> WebSocketConnection :
@@ -181,10 +181,27 @@ async def _close_connection(connection: WebSocketConnection) -> None:
181
181
except trio .TooSlowError :
182
182
raise DisconnectionTimeout from None
183
183
184
+ def _raise (exc : BaseException ) -> NoReturn :
185
+ """This helper allows re-raising an exception without __context__ being set."""
186
+ # cause does not need special handlng, we simply avoid using `raise .. from ..`
187
+ __tracebackhide__ = True
188
+ context = exc .__context__
189
+ try :
190
+ raise exc
191
+ finally :
192
+ exc .__context__ = context
193
+ del exc , context
194
+
184
195
connection : WebSocketConnection | None = None
185
196
close_result : outcome .Maybe [None ] | None = None
186
197
user_error = None
187
198
199
+ # Unwrapping exception groups has a lot of pitfalls, one of them stemming from
200
+ # the exception we raise also being inside the group that's set as the context.
201
+ # This leads to loss of info unless properly handled.
202
+ # See https://github.com/python-trio/flake8-async/issues/298
203
+ # We therefore avoid having the exceptiongroup included as either cause or context
204
+
188
205
try :
189
206
async with trio .open_nursery () as new_nursery :
190
207
result = await outcome .acapture (_open_connection , new_nursery )
@@ -205,7 +222,7 @@ async def _close_connection(connection: WebSocketConnection) -> None:
205
222
except _TRIO_EXC_GROUP_TYPE as e :
206
223
# user_error, or exception bubbling up from _reader_task
207
224
if len (e .exceptions ) == 1 :
208
- raise e .exceptions [0 ]
225
+ _raise ( e .exceptions [0 ])
209
226
210
227
# contains at most 1 non-cancelled exceptions
211
228
exception_to_raise : BaseException | None = None
@@ -218,25 +235,40 @@ async def _close_connection(connection: WebSocketConnection) -> None:
218
235
else :
219
236
if exception_to_raise is None :
220
237
# all exceptions are cancelled
221
- # prefer raising the one from the user, for traceback reasons
238
+ # we reraise the user exception and throw out internal
222
239
if user_error is not None :
223
- # no reason to raise from e, just to include a bunch of extra
224
- # cancelleds.
225
- raise user_error # pylint: disable=raise-missing-from
240
+ _raise (user_error )
226
241
# multiple internal Cancelled is not possible afaik
227
- raise e .exceptions [0 ] # pragma: no cover # pylint: disable=raise-missing-from
228
- raise exception_to_raise
242
+ # but if so we just raise one of them
243
+ _raise (e .exceptions [0 ]) # pragma: no cover
244
+ # raise the non-cancelled exception
245
+ _raise (exception_to_raise )
229
246
230
- # if we have any KeyboardInterrupt in the group, make sure to raise it.
247
+ # if we have any KeyboardInterrupt in the group, raise a new KeyboardInterrupt
248
+ # with the group as cause & context
231
249
for sub_exc in e .exceptions :
232
250
if isinstance (sub_exc , KeyboardInterrupt ):
233
- raise sub_exc from e
251
+ raise KeyboardInterrupt from e
234
252
235
253
# Both user code and internal code raised non-cancelled exceptions.
236
- # We "hide" the internal exception(s) in the __cause__ and surface
237
- # the user_error.
254
+ # We set the context to be an exception group containing internal exceptions
255
+ # and, if not None, ` user_error.__context__`
238
256
if user_error is not None :
239
- raise user_error from e
257
+ exceptions = [subexc for subexc in e .exceptions if subexc is not user_error ]
258
+ eg_substr = ''
259
+ # there's technically loss of info here, with __suppress_context__=True you
260
+ # still have original __context__ available, just not printed. But we delete
261
+ # it completely because we can't partially suppress the group
262
+ if user_error .__context__ is not None and not user_error .__suppress_context__ :
263
+ exceptions .append (user_error .__context__ )
264
+ eg_substr = ' and the context for the user exception'
265
+ eg_str = (
266
+ "Both internal and user exceptions encountered. This group contains "
267
+ "the internal exception(s)" + eg_substr + "."
268
+ )
269
+ user_error .__context__ = BaseExceptionGroup (eg_str , exceptions )
270
+ user_error .__suppress_context__ = False
271
+ _raise (user_error )
240
272
241
273
raise TrioWebsocketInternalError (
242
274
"The trio-websocket API is not expected to raise multiple exceptions. "
@@ -576,7 +608,7 @@ def __init__(self, reason):
576
608
:param reason:
577
609
:type reason: CloseReason
578
610
'''
579
- super ().__init__ ()
611
+ super ().__init__ (reason )
580
612
self .reason = reason
581
613
582
614
def __repr__ (self ):
@@ -596,7 +628,7 @@ def __init__(self, status_code, headers, body):
596
628
:param reason:
597
629
:type reason: CloseReason
598
630
'''
599
- super ().__init__ ()
631
+ super ().__init__ (status_code , headers , body )
600
632
#: a 3 digit HTTP status code
601
633
self .status_code = status_code
602
634
#: a tuple of 2-tuples containing header key/value pairs
0 commit comments