@@ -372,11 +372,22 @@ def _task_started(task: asyncio.Task) -> bool:
372
372
373
373
374
374
def is_anyio_cancellation (exc :CancelledError )-> bool :
375
- return (
376
- bool (exc .args )
377
- and isinstance (exc .args [0 ],str )
378
- and exc .args [0 ].startswith ("Cancelled by cancel scope " )
379
- )
375
+ # Sometimes third party frameworks catch a CancelledError and raise a new one, so as
376
+ # a workaround we have to look at the previous ones in __context__ too for a
377
+ # matching cancel message
378
+ while True :
379
+ if (
380
+ exc .args
381
+ and isinstance (exc .args [0 ],str )
382
+ and exc .args [0 ].startswith ("Cancelled by cancel scope " )
383
+ ):
384
+ return True
385
+
386
+ if isinstance (exc .__context__ ,CancelledError ):
387
+ exc = exc .__context__
388
+ continue
389
+
390
+ return False
380
391
381
392
382
393
class CancelScope (BaseCancelScope ):
@@ -397,8 +408,10 @@ def __init__(self, deadline: float = math.inf, shield: bool = False):
397
408
self ._cancel_handle :asyncio .Handle | None = None
398
409
self ._tasks :set [asyncio .Task ]= set ()
399
410
self ._host_task :asyncio .Task | None = None
400
- self ._cancel_calls :int = 0
401
- self ._cancelling :int | None = None
411
+ if sys .version_info >= (3 ,11 ):
412
+ self ._pending_uncancellations :int | None = 0
413
+ else :
414
+ self ._pending_uncancellations = None
402
415
403
416
def __enter__ (self )-> CancelScope :
404
417
if self ._active :
@@ -424,8 +437,6 @@ def __enter__(self) -> CancelScope:
424
437
425
438
self ._timeout ()
426
439
self ._active = True
427
- if sys .version_info >= (3 ,11 ):
428
- self ._cancelling = self ._host_task .cancelling ()
429
440
430
441
# Start cancelling the host task if the scope was cancelled before entering
431
442
if self ._cancel_called :
@@ -470,30 +481,41 @@ def __exit__(
470
481
471
482
host_task_state .cancel_scope = self ._parent_scope
472
483
473
- # Undo all cancellations done by this scope
474
- if self ._cancelling is not None :
475
- while self ._cancel_calls :
476
- self ._cancel_calls -= 1
477
- if self ._host_task .uncancel ()<= self ._cancelling :
478
- break
484
+ # Restart the cancellation effort in the closest visible, cancelled parent
485
+ # scope if necessary
486
+ self ._restart_cancellation_in_parent ()
479
487
480
488
# We only swallow the exception iff it was an AnyIO CancelledError, either
481
489
# directly as exc_val or inside an exception group and there are no cancelled
482
490
# parent cancel scopes visible to us here
483
- not_swallowed_exceptions = 0
484
- swallow_exception = False
485
- if exc_val is not None :
486
- for exc in iterate_exceptions (exc_val ):
487
- if self ._cancel_called and isinstance (exc ,CancelledError ):
488
- if not (swallow_exception := self ._uncancel (exc )):
489
- not_swallowed_exceptions += 1
490
- else :
491
- not_swallowed_exceptions += 1
491
+ if self ._cancel_called and not self ._parent_cancellation_is_visible_to_us :
492
+ # For each level-cancel() call made on the host task, call uncancel()
493
+ while self ._pending_uncancellations :
494
+ self ._host_task .uncancel ()
495
+ self ._pending_uncancellations -= 1
496
+
497
+ # Update cancelled_caught and check for exceptions we must not swallow
498
+ cannot_swallow_exc_val = False
499
+ if exc_val is not None :
500
+ for exc in iterate_exceptions (exc_val ):
501
+ if isinstance (exc ,CancelledError )and is_anyio_cancellation (
502
+ exc
503
+ ):
504
+ self ._cancelled_caught = True
505
+ else :
506
+ cannot_swallow_exc_val = True
492
507
493
- # Restart the cancellation effort in the closest visible, cancelled parent
494
- # scope if necessary
495
- self ._restart_cancellation_in_parent ()
496
- return swallow_exception and not not_swallowed_exceptions
508
+ return self ._cancelled_caught and not cannot_swallow_exc_val
509
+ else :
510
+ if self ._pending_uncancellations :
511
+ assert self ._parent_scope is not None
512
+ assert self ._parent_scope ._pending_uncancellations is not None
513
+ self ._parent_scope ._pending_uncancellations += (
514
+ self ._pending_uncancellations
515
+ )
516
+ self ._pending_uncancellations = 0
517
+
518
+ return False
497
519
finally :
498
520
self ._host_task = None
499
521
del exc_val
@@ -520,31 +542,6 @@ def _parent_cancellation_is_visible_to_us(self) -> bool:
520
542
and self ._parent_scope ._effectively_cancelled
521
543
)
522
544
523
- def _uncancel (self ,cancelled_exc :CancelledError )-> bool :
524
- if self ._host_task is None :
525
- self ._cancel_calls = 0
526
- return True
527
-
528
- while True :
529
- if is_anyio_cancellation (cancelled_exc ):
530
- # Only swallow the cancellation exception if it's an AnyIO cancel
531
- # exception and there are no other cancel scopes down the line pending
532
- # cancellation
533
- self ._cancelled_caught = (
534
- self ._effectively_cancelled
535
- and not self ._parent_cancellation_is_visible_to_us
536
- )
537
- return self ._cancelled_caught
538
-
539
- # Sometimes third party frameworks catch a CancelledError and raise a new
540
- # one, so as a workaround we have to look at the previous ones in
541
- # __context__ too for a matching cancel message
542
- if isinstance (cancelled_exc .__context__ ,CancelledError ):
543
- cancelled_exc = cancelled_exc .__context__
544
- continue
545
-
546
- return False
547
-
548
545
def _timeout (self )-> None :
549
546
if self ._deadline != math .inf :
550
547
loop = get_running_loop ()
@@ -576,8 +573,11 @@ def _deliver_cancellation(self, origin: CancelScope) -> bool:
576
573
waiter = task ._fut_waiter # type: ignore[attr-defined]
577
574
if not isinstance (waiter ,asyncio .Future )or not waiter .done ():
578
575
task .cancel (f"Cancelled by cancel scope{ id (origin ):x} " )
579
- if task is origin ._host_task :
580
- origin ._cancel_calls += 1
576
+ if (
577
+ task is origin ._host_task
578
+ and origin ._pending_uncancellations is not None
579
+ ):
580
+ origin ._pending_uncancellations += 1
581
581
582
582
# Deliver cancellation to child scopes that aren't shielded or running their own
583
583
# cancellation callbacks
@@ -2154,12 +2154,11 @@ def has_pending_cancellation(self) -> bool:
2154
2154
# If the task isn't around anymore, it won't have a pending cancellation
2155
2155
return False
2156
2156
2157
- if sys .version_info >= (3 ,11 ):
2158
- if task .cancelling ():
2159
- return True
2157
+ if task ._must_cancel :# type: ignore[attr-defined]
2158
+ return True
2160
2159
elif (
2161
- isinstance (task ._fut_waiter ,asyncio .Future )
2162
- and task ._fut_waiter .cancelled ()
2160
+ isinstance (task ._fut_waiter ,asyncio .Future )# type: ignore[attr-defined]
2161
+ and task ._fut_waiter .cancelled ()# type: ignore[attr-defined]
2163
2162
):
2164
2163
return True
2165
2164