@@ -372,11 +372,22 @@ def _task_started(task: asyncio.Task) -> bool:
372372
373373
374374def 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
380391
381392
382393class CancelScope (BaseCancelScope ):
@@ -397,8 +408,10 @@ def __init__(self, deadline: float = math.inf, shield: bool = False):
397408 self ._cancel_handle : asyncio .Handle | None = None
398409 self ._tasks : set [asyncio .Task ] = set ()
399410 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
402415
403416 def __enter__ (self ) -> CancelScope :
404417 if self ._active :
@@ -424,8 +437,6 @@ def __enter__(self) -> CancelScope:
424437
425438 self ._timeout ()
426439 self ._active = True
427- if sys .version_info >= (3 , 11 ):
428- self ._cancelling = self ._host_task .cancelling ()
429440
430441 # Start cancelling the host task if the scope was cancelled before entering
431442 if self ._cancel_called :
@@ -470,30 +481,41 @@ def __exit__(
470481
471482 host_task_state .cancel_scope = self ._parent_scope
472483
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 ()
479487
480488 # We only swallow the exception iff it was an AnyIO CancelledError, either
481489 # directly as exc_val or inside an exception group and there are no cancelled
482490 # 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
492507
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
497519 finally :
498520 self ._host_task = None
499521 del exc_val
@@ -520,31 +542,6 @@ def _parent_cancellation_is_visible_to_us(self) -> bool:
520542 and self ._parent_scope ._effectively_cancelled
521543 )
522544
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-
548545 def _timeout (self ) -> None :
549546 if self ._deadline != math .inf :
550547 loop = get_running_loop ()
@@ -576,8 +573,11 @@ def _deliver_cancellation(self, origin: CancelScope) -> bool:
576573 waiter = task ._fut_waiter # type: ignore[attr-defined]
577574 if not isinstance (waiter , asyncio .Future ) or not waiter .done ():
578575 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
581581
582582 # Deliver cancellation to child scopes that aren't shielded or running their own
583583 # cancellation callbacks
@@ -2154,12 +2154,11 @@ def has_pending_cancellation(self) -> bool:
21542154 # If the task isn't around anymore, it won't have a pending cancellation
21552155 return False
21562156
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
21602159 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]
21632162 ):
21642163 return True
21652164
0 commit comments