36
36
from ._asyncgens import AsyncGenerators
37
37
from ._concat_tb import concat_tb
38
38
from ._entry_queue import EntryQueue , TrioToken
39
- from ._exceptions import Cancelled , RunFinishedError , TrioInternalError
39
+ from ._exceptions import (
40
+ Cancelled ,
41
+ CancelReasonLiteral ,
42
+ RunFinishedError ,
43
+ TrioInternalError ,
44
+ )
40
45
from ._instrumentation import Instruments
41
46
from ._ki import KIManager , enable_ki_protection
42
47
from ._parking_lot import GLOBAL_PARKING_LOT_BREAKER
@@ -305,7 +310,7 @@ def expire(self, now: float) -> bool:
305
310
did_something = True
306
311
# This implicitly calls self.remove(), so we don't need to
307
312
# decrement _active here
308
- cancel_scope .cancel ( )
313
+ cancel_scope ._cancel ( CancelReason ( source = "deadline" ) )
309
314
# If we've accumulated too many stale entries, then prune the heap to
310
315
# keep it under control. (We only do this occasionally in a batch, to
311
316
# keep the amortized cost down)
@@ -314,6 +319,20 @@ def expire(self, now: float) -> bool:
314
319
return did_something
315
320
316
321
322
+ @attrs .define
323
+ class CancelReason :
324
+ """Attached to a :class:`CancelScope` upon cancellation with details of the source of the
325
+ cancellation, which is then used to construct the string in a :exc:`Cancelled`.
326
+ Users can pass a ``reason`` str to :meth:`CancelScope.cancel` to set it.
327
+
328
+ Not publicly exported or documented.
329
+ """
330
+
331
+ source : CancelReasonLiteral
332
+ source_task : str | None = None
333
+ reason : str | None = None
334
+
335
+
317
336
@attrs .define (eq = False )
318
337
class CancelStatus :
319
338
"""Tracks the cancellation status for a contiguous extent
@@ -468,6 +487,14 @@ def recalculate(self) -> None:
468
487
or current .parent_cancellation_is_visible_to_us
469
488
)
470
489
if new_state != current .effectively_cancelled :
490
+ if (
491
+ current ._scope ._cancel_reason is None
492
+ and current .parent_cancellation_is_visible_to_us
493
+ ):
494
+ assert current ._parent is not None
495
+ current ._scope ._cancel_reason = (
496
+ current ._parent ._scope ._cancel_reason
497
+ )
471
498
current .effectively_cancelled = new_state
472
499
if new_state :
473
500
for task in current ._tasks :
@@ -558,6 +585,8 @@ class CancelScope:
558
585
_cancel_called : bool = attrs .field (default = False , init = False )
559
586
cancelled_caught : bool = attrs .field (default = False , init = False )
560
587
588
+ _cancel_reason : CancelReason | None = attrs .field (default = None , init = False )
589
+
561
590
# Constructor arguments:
562
591
_relative_deadline : float = attrs .field (
563
592
default = inf ,
@@ -594,7 +623,7 @@ def __enter__(self) -> Self:
594
623
self ._relative_deadline = inf
595
624
596
625
if current_time () >= self ._deadline :
597
- self .cancel ( )
626
+ self ._cancel ( CancelReason ( source = "deadline" ) )
598
627
with self ._might_change_registered_deadline ():
599
628
self ._cancel_status = CancelStatus (scope = self , parent = task ._cancel_status )
600
629
task ._activate_cancel_status (self ._cancel_status )
@@ -883,19 +912,42 @@ def shield(self, new_value: bool) -> None:
883
912
self ._cancel_status .recalculate ()
884
913
885
914
@enable_ki_protection
886
- def cancel (self ) -> None :
887
- """Cancels this scope immediately.
888
-
889
- This method is idempotent, i.e., if the scope was already
890
- cancelled then this method silently does nothing.
915
+ def _cancel (self , cancel_reason : CancelReason | None ) -> None :
916
+ """Internal sources of cancellation should use this instead of :meth:`cancel`
917
+ in order to set a more detailed :class:`CancelReason`
918
+ Helper or high-level functions can use `cancel`.
891
919
"""
892
920
if self ._cancel_called :
893
921
return
922
+
923
+ if self ._cancel_reason is None :
924
+ self ._cancel_reason = cancel_reason
925
+
894
926
with self ._might_change_registered_deadline ():
895
927
self ._cancel_called = True
928
+
896
929
if self ._cancel_status is not None :
897
930
self ._cancel_status .recalculate ()
898
931
932
+ @enable_ki_protection
933
+ def cancel (self , reason : str | None = None ) -> None :
934
+ """Cancels this scope immediately.
935
+
936
+ The optional ``reason`` argument accepts a string, which will be attached to
937
+ any resulting :exc:`Cancelled` exception to help you understand where that
938
+ cancellation is coming from and why it happened.
939
+
940
+ This method is idempotent, i.e., if the scope was already
941
+ cancelled then this method silently does nothing.
942
+ """
943
+ try :
944
+ current_task = repr (_core .current_task ())
945
+ except RuntimeError :
946
+ current_task = None
947
+ self ._cancel (
948
+ CancelReason (reason = reason , source = "explicit" , source_task = current_task )
949
+ )
950
+
899
951
@property
900
952
def cancel_called (self ) -> bool :
901
953
"""Readonly :class:`bool`. Records whether cancellation has been
@@ -924,7 +976,7 @@ def cancel_called(self) -> bool:
924
976
# but it makes the value returned by cancel_called more
925
977
# closely match expectations.
926
978
if not self ._cancel_called and current_time () >= self ._deadline :
927
- self .cancel ( )
979
+ self ._cancel ( CancelReason ( source = "deadline" ) )
928
980
return self ._cancel_called
929
981
930
982
@@ -1192,9 +1244,9 @@ def parent_task(self) -> Task:
1192
1244
"(`~trio.lowlevel.Task`): The Task that opened this nursery."
1193
1245
return self ._parent_task
1194
1246
1195
- def _add_exc (self , exc : BaseException ) -> None :
1247
+ def _add_exc (self , exc : BaseException , reason : CancelReason | None ) -> None :
1196
1248
self ._pending_excs .append (exc )
1197
- self .cancel_scope .cancel ( )
1249
+ self .cancel_scope ._cancel ( reason )
1198
1250
1199
1251
def _check_nursery_closed (self ) -> None :
1200
1252
if not any ([self ._nested_child_running , self ._children , self ._pending_starts ]):
@@ -1210,7 +1262,14 @@ def _child_finished(
1210
1262
) -> None :
1211
1263
self ._children .remove (task )
1212
1264
if isinstance (outcome , Error ):
1213
- self ._add_exc (outcome .error )
1265
+ self ._add_exc (
1266
+ outcome .error ,
1267
+ CancelReason (
1268
+ source = "nursery" ,
1269
+ source_task = repr (task ),
1270
+ reason = f"child task raised exception { outcome .error !r} " ,
1271
+ ),
1272
+ )
1214
1273
self ._check_nursery_closed ()
1215
1274
1216
1275
async def _nested_child_finished (
@@ -1220,7 +1279,14 @@ async def _nested_child_finished(
1220
1279
# Returns ExceptionGroup instance (or any exception if the nursery is in loose mode
1221
1280
# and there is just one contained exception) if there are pending exceptions
1222
1281
if nested_child_exc is not None :
1223
- self ._add_exc (nested_child_exc )
1282
+ self ._add_exc (
1283
+ nested_child_exc ,
1284
+ reason = CancelReason (
1285
+ source = "nursery" ,
1286
+ source_task = repr (self ._parent_task ),
1287
+ reason = f"Code block inside nursery contextmanager raised exception { nested_child_exc !r} " ,
1288
+ ),
1289
+ )
1224
1290
self ._nested_child_running = False
1225
1291
self ._check_nursery_closed ()
1226
1292
@@ -1231,7 +1297,13 @@ async def _nested_child_finished(
1231
1297
def aborted (raise_cancel : _core .RaiseCancelT ) -> Abort :
1232
1298
exn = capture (raise_cancel ).error
1233
1299
if not isinstance (exn , Cancelled ):
1234
- self ._add_exc (exn )
1300
+ self ._add_exc (
1301
+ exn ,
1302
+ CancelReason (
1303
+ source = "KeyboardInterrupt" ,
1304
+ source_task = repr (self ._parent_task ),
1305
+ ),
1306
+ )
1235
1307
# see test_cancel_scope_exit_doesnt_create_cyclic_garbage
1236
1308
del exn # prevent cyclic garbage creation
1237
1309
return Abort .FAILED
@@ -1245,7 +1317,8 @@ def aborted(raise_cancel: _core.RaiseCancelT) -> Abort:
1245
1317
try :
1246
1318
await cancel_shielded_checkpoint ()
1247
1319
except BaseException as exc :
1248
- self ._add_exc (exc )
1320
+ # there's no children to cancel, so don't need to supply cancel reason
1321
+ self ._add_exc (exc , reason = None )
1249
1322
1250
1323
popped = self ._parent_task ._child_nurseries .pop ()
1251
1324
assert popped is self
@@ -1575,8 +1648,17 @@ def _attempt_delivery_of_any_pending_cancel(self) -> None:
1575
1648
if not self ._cancel_status .effectively_cancelled :
1576
1649
return
1577
1650
1651
+ reason = self ._cancel_status ._scope ._cancel_reason
1652
+
1578
1653
def raise_cancel () -> NoReturn :
1579
- raise Cancelled ._create ()
1654
+ if reason is None :
1655
+ raise Cancelled ._create (source = "unknown" , reason = "misnesting" )
1656
+ else :
1657
+ raise Cancelled ._create (
1658
+ source = reason .source ,
1659
+ reason = reason .reason ,
1660
+ source_task = reason .source_task ,
1661
+ )
1580
1662
1581
1663
self ._attempt_abort (raise_cancel )
1582
1664
@@ -2075,15 +2157,27 @@ async def init(
2075
2157
)
2076
2158
2077
2159
# Main task is done; start shutting down system tasks
2078
- self .system_nursery .cancel_scope .cancel ()
2160
+ self .system_nursery .cancel_scope ._cancel (
2161
+ CancelReason (
2162
+ source = "shutdown" ,
2163
+ reason = "main task done, shutting down system tasks" ,
2164
+ source_task = repr (self .init_task ),
2165
+ )
2166
+ )
2079
2167
2080
2168
# System nursery is closed; finalize remaining async generators
2081
2169
await self .asyncgens .finalize_remaining (self )
2082
2170
2083
2171
# There are no more asyncgens, which means no more user-provided
2084
2172
# code except possibly run_sync_soon callbacks. It's finally safe
2085
2173
# to stop the run_sync_soon task and exit run().
2086
- run_sync_soon_nursery .cancel_scope .cancel ()
2174
+ run_sync_soon_nursery .cancel_scope ._cancel (
2175
+ CancelReason (
2176
+ source = "shutdown" ,
2177
+ reason = "main task done, shutting down run_sync_soon callbacks" ,
2178
+ source_task = repr (self .init_task ),
2179
+ )
2180
+ )
2087
2181
2088
2182
################
2089
2183
# Outside context problems
@@ -2926,7 +3020,18 @@ async def checkpoint() -> None:
2926
3020
if task ._cancel_status .effectively_cancelled or (
2927
3021
task is task ._runner .main_task and task ._runner .ki_pending
2928
3022
):
2929
- with CancelScope (deadline = - inf ):
3023
+ cs = CancelScope (deadline = - inf )
3024
+ if (
3025
+ task ._cancel_status ._scope ._cancel_reason is None
3026
+ and task is task ._runner .main_task
3027
+ and task ._runner .ki_pending
3028
+ ):
3029
+ task ._cancel_status ._scope ._cancel_reason = CancelReason (
3030
+ source = "KeyboardInterrupt"
3031
+ )
3032
+ assert task ._cancel_status ._scope ._cancel_reason is not None
3033
+ cs ._cancel_reason = task ._cancel_status ._scope ._cancel_reason
3034
+ with cs :
2930
3035
await _core .wait_task_rescheduled (lambda _ : _core .Abort .SUCCEEDED )
2931
3036
2932
3037
0 commit comments