Skip to content

Commit

Permalink
Merge pull request #1558 from oremanj/eventual-parent
Browse files Browse the repository at this point in the history
Add Task.eventual_parent_nursery introspection attribute
  • Loading branch information
njsmith authored May 29, 2020
2 parents 4081a05 + 9c6dff8 commit 7e6cf19
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 9 deletions.
6 changes: 4 additions & 2 deletions docs/source/reference-lowlevel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,11 @@ Windows-specific API

.. function:: WaitForSingleObject(handle)
:async:

Async and cancellable variant of `WaitForSingleObject
<https://msdn.microsoft.com/en-us/library/windows/desktop/ms687032(v=vs.85).aspx>`__.
Windows only.

:arg handle:
A Win32 object handle, as a Python integer.
:raises OSError:
Expand Down Expand Up @@ -517,6 +517,8 @@ Task API

.. autoattribute:: parent_nursery

.. autoattribute:: eventual_parent_nursery

.. autoattribute:: child_nurseries

.. attribute:: custom_sleep_data
Expand Down
11 changes: 11 additions & 0 deletions newsfragments/1558.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Tasks spawned with `nursery.start() <trio.Nursery.start>` aren't treated as
direct children of their nursery until they call ``task_status.started()``.
This is visible through the task tree introspection attributes such as
`Task.parent_nursery <trio.lowlevel.Task.parent_nursery>`. Sometimes, though,
you want to know where the task is going to wind up, even if it hasn't finished
initializing yet. To support this, we added a new attribute
`Task.eventual_parent_nursery <trio.lowlevel.Task.eventual_parent_nursery>`.
For a task spawned with :meth:`~trio.Nursery.start` that hasn't yet called
``started()``, this is the nursery that the task was nominally started in,
where it will be running once it finishes starting up. In all other cases,
it is ``None``.
23 changes: 21 additions & 2 deletions trio/_core/_run.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# coding: utf-8

import functools
import itertools
import logging
Expand Down Expand Up @@ -659,6 +661,7 @@ def started(self, value=None):
self._old_nursery._children = set()
for task in tasks:
task._parent_nursery = self._new_nursery
task._eventual_parent_nursery = None
self._new_nursery._children.add(task)

# Move all children of the old nursery's cancel status object
Expand Down Expand Up @@ -862,7 +865,7 @@ def start_soon(self, async_fn, *args, name=None):
If you want to run a function and immediately wait for its result,
then you don't need a nursery; just use ``await async_fn(*args)``.
If you want to wait for the task to initialize itself before
continuing, see :meth:`start()`.
continuing, see :meth:`start`.
It's possible to pass a nursery object into another task, which
allows that task to start new child tasks in the first task's
Expand Down Expand Up @@ -942,7 +945,10 @@ async def async_fn(arg1, arg2, \*, task_status=trio.TASK_STATUS_IGNORED):
async with open_nursery() as old_nursery:
task_status = _TaskStatus(old_nursery, self)
thunk = functools.partial(async_fn, task_status=task_status)
old_nursery.start_soon(thunk, *args, name=name)
task = GLOBAL_RUN_CONTEXT.runner.spawn_impl(
thunk, args, old_nursery, name
)
task._eventual_parent_nursery = self
# Wait for either _TaskStatus.started or an exception to
# cancel this nursery:
# If we get here, then the child either got reparented or exited
Expand Down Expand Up @@ -992,6 +998,7 @@ class Task(metaclass=NoPublicConstructor):

# For introspection and nursery.start()
_child_nurseries = attr.ib(factory=list)
_eventual_parent_nursery = attr.ib(default=None)

# these are counts of how many cancel/schedule points this task has
# executed, for assert{_no,}_checkpoints
Expand All @@ -1013,6 +1020,18 @@ def parent_nursery(self):
"""
return self._parent_nursery

@property
def eventual_parent_nursery(self):
"""The nursery this task will be inside after it calls
``task_status.started()``.
If this task has already called ``started()``, or if it was not
spawned using `nursery.start() <trio.Nursery.start>`, then
its `eventual_parent_nursery` is ``None``.
"""
return self._eventual_parent_nursery

@property
def child_nurseries(self):
"""The nurseries this task contains.
Expand Down
26 changes: 21 additions & 5 deletions trio/_core/tests/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ async def child():

async def test_root_task():
root = _core.current_root_task()
assert root.parent_nursery is None
assert root.parent_nursery is root.eventual_parent_nursery is None


def test_out_of_context():
Expand Down Expand Up @@ -1588,7 +1588,7 @@ async def test_task_tree_introspection():
tasks = {}
nurseries = {}

async def parent():
async def parent(task_status=_core.TASK_STATUS_IGNORED):
tasks["parent"] = _core.current_task()

assert tasks["parent"].child_nurseries == []
Expand All @@ -1601,7 +1601,7 @@ async def parent():

async with _core.open_nursery() as nursery:
nurseries["parent"] = nursery
nursery.start_soon(child1)
await nursery.start(child1)

# Upward links survive after tasks/nurseries exit
assert nurseries["parent"].parent_task is tasks["parent"]
Expand All @@ -1624,15 +1624,31 @@ async def child2():
assert nurseries["child1"].child_tasks == frozenset({tasks["child2"]})
assert tasks["child2"].child_nurseries == []

async def child1():
tasks["child1"] = _core.current_task()
async def child1(task_status=_core.TASK_STATUS_IGNORED):
me = tasks["child1"] = _core.current_task()
assert me.parent_nursery.parent_task is tasks["parent"]
assert me.parent_nursery is not nurseries["parent"]
assert me.eventual_parent_nursery is nurseries["parent"]
task_status.started()
assert me.parent_nursery is nurseries["parent"]
assert me.eventual_parent_nursery is None

# Wait for the start() call to return and close its internal nursery, to
# ensure consistent results in child2:
await _core.wait_all_tasks_blocked()

async with _core.open_nursery() as nursery:
nurseries["child1"] = nursery
nursery.start_soon(child2)

async with _core.open_nursery() as nursery:
nursery.start_soon(parent)

# There are no pending starts, so no one should have a non-None
# eventual_parent_nursery
for task in tasks.values():
assert task.eventual_parent_nursery is None


async def test_nursery_closure():
async def child1(nursery):
Expand Down

0 comments on commit 7e6cf19

Please sign in to comment.