diff --git a/docs/source/reference-core.rst b/docs/source/reference-core.rst index 2e09da8bb4..0e266ae6c4 100644 --- a/docs/source/reference-core.rst +++ b/docs/source/reference-core.rst @@ -956,9 +956,14 @@ Nursery objects provide the following interface: other things, e.g. if you want to explicitly cancel all children in response to some external event. + The last two attributes are mainly to enable introspection of the + task tree, for example in debuggers. + .. attribute:: parent_task + The :class:`~trio.Task` that opened this nursery. + .. attribute:: child_tasks A :class:`frozenset` containing all the child :class:`~trio.Task` objects which are still running. @@ -1013,6 +1018,10 @@ Task object API .. autoattribute:: parent_task + .. autoattribute:: parent_nursery + + .. autoattribute:: child_nurseries + Working with :exc:`MultiError`\s ++++++++++++++++++++++++++++++++ diff --git a/trio/_core/_run.py b/trio/_core/_run.py index 4da5802fc2..96c55ec5d2 100644 --- a/trio/_core/_run.py +++ b/trio/_core/_run.py @@ -344,14 +344,14 @@ async def open_nursery(): class Nursery: - def __init__(self, parent, cancel_scope): + def __init__(self, parent_task, cancel_scope): # the parent task -- only used for introspection, to implement # task.parent_task - self._parent = parent - parent._child_nurseries.append(self) + self._parent_task = parent_task + parent_task._child_nurseries.append(self) # the cancel stack that children inherit - we take a snapshot, so it # won't be affected by any changes in the parent. - self._cancel_stack = list(parent._cancel_stack) + self._cancel_stack = list(parent_task._cancel_stack) # the cancel scope that directly surrounds us; used for cancelling all # children. self.cancel_scope = cancel_scope @@ -363,9 +363,18 @@ def __init__(self, parent, cancel_scope): self._closed = False @property + @deprecated("0.2.0", instead="child_tasks", issue=136) def children(self): return frozenset(self._children) + @property + def child_tasks(self): + return frozenset(self._children) + + @property + def parent_task(self): + return self._parent_task + @property @deprecated("0.2.0", instead=None, issue=136) def zombies(self): @@ -474,7 +483,7 @@ async def _clean_up(self, pending_exc): exceptions.append(exc) self._closed = True - popped = self._parent._child_nurseries.pop() + popped = self._parent_task._child_nurseries.pop() assert popped is self if exceptions: mexc = MultiError(exceptions) @@ -563,6 +572,7 @@ def result(self): # For debugging and visualization: @property + @deprecated("0.2.0", instead="parent_nursery.parent_task", issue=136) def parent_task(self): """This task's parent task (or None if this is the "init" task). @@ -571,7 +581,27 @@ def parent_task(self): if self._parent_nursery is None: return None else: - return self._parent_nursery._parent + return self._parent_nursery._parent_task + + @property + def parent_nursery(self): + """The nursery this task is inside (or None if this is the "init" + take). + + Example use case: drawing a visualization of the task tree in a + debugger. + + """ + return self._parent_nursery + + @property + def child_nurseries(self): + """The nurseries this task contains. + + This is a list, with outer nurseries before inner nurseries. + + """ + return list(self._child_nurseries) ################ # Monitoring task exit diff --git a/trio/_core/tests/test_run.py b/trio/_core/tests/test_run.py index a4d3d7fb71..50cdc65fb1 100644 --- a/trio/_core/tests/test_run.py +++ b/trio/_core/tests/test_run.py @@ -356,7 +356,7 @@ async def test_current_task(): parent_task = _core.current_task() async def child(): - assert _core.current_task().parent_task is parent_task + assert _core.current_task().parent_nursery.parent_task is parent_task async with _core.open_nursery() as nursery: nursery.start_soon(child) @@ -1493,7 +1493,7 @@ def slow_abort(raise_cancel): assert record == ["sleeping", "abort-called", "cancelled", "done"] -async def test_parent_task(): +async def test_Task_parent_task_deprecated(recwarn): tasks = {} async def child2(): @@ -1517,6 +1517,53 @@ async def child1(): t = t.parent_task +async def test_task_tree_introspection(): + tasks = {} + + tasks["parent"] = _core.current_task() + + assert tasks["parent"].child_nurseries == [] + + async with _core.open_nursery() as nursery1: + async with _core.open_nursery() as nursery2: + assert tasks["parent"].child_nurseries == [nursery1, nursery2] + + assert tasks["parent"].child_nurseries == [] + + nurseries = {} + + async def child2(): + tasks["child2"] = _core.current_task() + assert tasks["parent"].child_nurseries == [nurseries["parent"]] + assert nurseries["parent"].child_tasks == frozenset({tasks["child1"]}) + assert tasks["child1"].child_nurseries == [nurseries["child1"]] + assert nurseries["child1"].child_tasks == frozenset({tasks["child2"]}) + assert tasks["child2"].child_nurseries == [] + + async def child1(): + tasks["child1"] = _core.current_task() + async with _core.open_nursery() as nursery: + nurseries["child1"] = nursery + nursery.start_soon(child2) + + async with _core.open_nursery() as nursery: + nurseries["parent"] = nursery + nursery.start_soon(child1) + + # Upward links survive after tasks/nurseries exit + assert nurseries["parent"].parent_task is tasks["parent"] + assert tasks["child1"].parent_nursery is nurseries["parent"] + assert nurseries["child1"].parent_task is tasks["child1"] + assert tasks["child2"].parent_nursery is nurseries["child1"] + + nursery = _core.current_task().parent_nursery + # Make sure that chaining eventually gives a nursery of None (and not, for + # example, an error) + while nursery is not None: + t = nursery.parent_task + nursery = t.parent_nursery + + async def test_nursery_closure(): async def child1(nursery): # We can add new tasks to the nursery even after entering __aexit__, @@ -1694,11 +1741,11 @@ async def sleep_then_start(seconds, *, task_status=_core.STATUS_IGNORED): # to exit. for seconds in [1, 2]: async with _core.open_nursery() as nursery: - assert len(nursery.children) == 0 + assert len(nursery.child_tasks) == 0 t0 = _core.current_time() assert await nursery.start(sleep_then_start, seconds) == seconds assert _core.current_time() - t0 == seconds - assert len(nursery.children) == 1 + assert len(nursery.child_tasks) == 1 assert _core.current_time() - t0 == 2 * seconds # Make sure STATUS_IGNORED works so task function can be called directly diff --git a/trio/tests/test_highlevel_serve_listeners.py b/trio/tests/test_highlevel_serve_listeners.py index 173893f54a..025fa7474e 100644 --- a/trio/tests/test_highlevel_serve_listeners.py +++ b/trio/tests/test_highlevel_serve_listeners.py @@ -128,7 +128,7 @@ async def connection_watcher(*, task_status=trio.STATUS_IGNORED): async with trio.open_nursery() as nursery: task_status.started(nursery) await wait_all_tasks_blocked() - assert len(nursery.children) == 10 + assert len(nursery.child_tasks) == 10 raise Done with pytest.raises(Done):