File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -526,6 +526,8 @@ def get_exception(self) -> TaskFailedError:
526526class CompositeTask (Task [T ]):
527527 """A task that is composed of other tasks."""
528528 _tasks : list [Task [Any ]]
529+ _completed_tasks : int
530+ _failed_tasks : int
529531
530532 def __init__ (self , tasks : list [Task [Any ]]):
531533 super ().__init__ ()
@@ -549,10 +551,14 @@ class WhenAllTask(CompositeTask[list[T]]):
549551 """A task that completes when all of its child tasks complete."""
550552
551553 def __init__ (self , tasks : list [Task [T ]]):
552- super ().__init__ (cast (list [Task [Any ]], tasks ))
553- self ._completed_tasks = 0
554- self ._failed_tasks = 0
554+ # Initialize state that on_child_completed() reads BEFORE invoking the
555+ # base constructor: CompositeTask.__init__ calls on_child_completed()
556+ # for any children that are already complete, so `_pending_exception`
557+ # must exist first. The base constructor also initializes
558+ # `_completed_tasks`/`_failed_tasks` to 0 and then accounts for
559+ # pre-completed children, so they must not be reset afterwards.
555560 self ._pending_exception : TaskFailedError | None = None
561+ super ().__init__ (cast (list [Task [Any ]], tasks ))
556562
557563 @property
558564 def pending_tasks (self ) -> int :
Original file line number Diff line number Diff line change @@ -1680,6 +1680,33 @@ def test_when_all_defers_failure_until_all_children_complete():
16801680 assert when_all .is_failed
16811681
16821682
1683+ def test_when_all_handles_pre_completed_children ():
1684+ """when_all must account for children that are already complete/failed.
1685+
1686+ CompositeTask.__init__ invokes on_child_completed() for children that are
1687+ already complete, so when_all must be constructible from an already-failed
1688+ child without raising (regression test for AttributeError on
1689+ `_pending_exception`).
1690+ """
1691+ # An already-failed child plus a still-pending child.
1692+ failed = task .CompletableTask ()
1693+ failed .fail ("boom" , Exception ("boom" ))
1694+ pending = task .CompletableTask ()
1695+
1696+ when_all = task .when_all ([failed , pending ])
1697+
1698+ # The pre-completed failure is accounted for but not yet surfaced.
1699+ assert when_all .get_completed_tasks () == 1
1700+ assert not when_all .is_complete
1701+ assert not when_all .is_failed
1702+
1703+ # Completing the remaining child finishes the composite and surfaces the
1704+ # first failure.
1705+ pending .complete ("ok" )
1706+ assert when_all .is_complete
1707+ assert when_all .is_failed
1708+
1709+
16831710def test_when_any ():
16841711 """Tests that a when_any pattern works correctly"""
16851712 def hello (_ , name : str ):
You can’t perform that action at this time.
0 commit comments