@@ -856,20 +856,35 @@ def started(self: _TaskStatus[StatusT], value: StatusT) -> None:
856
856
def started (self , value : StatusT | None = None ) -> None :
857
857
if self ._value is not _NoStatus :
858
858
raise RuntimeError ("called 'started' twice on the same task status" )
859
- self ._value = cast (StatusT , value ) # If None, StatusT == None
860
859
861
- # If the old nursery is cancelled, then quietly quit now; the child
862
- # will eventually exit on its own, and we don't want to risk moving
863
- # children that might have propagating Cancelled exceptions into
864
- # a place with no cancelled cancel scopes to catch them.
865
- assert self ._old_nursery ._cancel_status is not None
866
- if self ._old_nursery ._cancel_status .effectively_cancelled :
867
- return
860
+ # Make sure we don't move a task with propagating Cancelled exception(s)
861
+ # to a place in the tree without the corresponding cancel scope(s).
862
+ #
863
+ # N.B.: This check is limited to the task that calls started(). If the
864
+ # user uses lowlevel.current_task().parent_nursery to add other tasks to
865
+ # the private implementation-detail nursery of start(), this won't be
866
+ # able to check those tasks. See #1599.
867
+ _ , exc , _ = sys .exc_info ()
868
+ while exc is not None :
869
+ handling_cancelled = False
870
+ if isinstance (exc , Cancelled ):
871
+ handling_cancelled = True
872
+ elif isinstance (exc , BaseExceptionGroup ):
873
+ matched , _ = exc .split (Cancelled )
874
+ if matched :
875
+ handling_cancelled = True
876
+ if handling_cancelled :
877
+ raise RuntimeError (
878
+ "task_status.started() cannot be called while handling Cancelled(s)"
879
+ )
880
+ exc = exc .__context__
868
881
869
882
# Can't be closed, b/c we checked in start() and then _pending_starts
870
883
# should keep it open.
871
884
assert not self ._new_nursery ._closed
872
885
886
+ self ._value = cast (StatusT , value ) # If None, StatusT == None
887
+
873
888
# Move tasks from the old nursery to the new
874
889
tasks = self ._old_nursery ._children
875
890
self ._old_nursery ._children = set ()
@@ -1209,6 +1224,12 @@ async def async_fn(arg1, arg2, *, task_status=trio.TASK_STATUS_IGNORED):
1209
1224
1210
1225
If the child task passes a value to :meth:`task_status.started(value) <TaskStatus.started>`,
1211
1226
then :meth:`start` returns this value. Otherwise, it returns ``None``.
1227
+
1228
+ :meth:`task_status.started() <TaskStatus.started>` cannot be called by
1229
+ an exception handler (or other cleanup code, like ``finally`` blocks,
1230
+ ``__aexit__`` handlers, and so on) that is handling one or more
1231
+ :exc:`Cancelled` exceptions. (It'll raise a :exc:`RuntimeError` if you
1232
+ violate this rule.)
1212
1233
"""
1213
1234
if self ._closed :
1214
1235
raise RuntimeError ("Nursery is closed to new arrivals" )
0 commit comments