From c149ea42e2b6433b50af38541d65ccb2dff7cb5d Mon Sep 17 00:00:00 2001 From: benjotron Date: Fri, 7 Jun 2019 20:18:30 -0500 Subject: [PATCH 1/4] Privatize nurseries --- trio/_core/_run.py | 6 +++--- trio/_core/tests/test_run.py | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/trio/_core/_run.py b/trio/_core/_run.py index fb239304ef..8b654a90b3 100644 --- a/trio/_core/_run.py +++ b/trio/_core/_run.py @@ -37,7 +37,7 @@ ) from .. import _core from .._deprecate import deprecated -from .._util import Final +from .._util import Final, NoPublicConstructor # At the bottom of this file there's also some "clever" code that generates # wrapper functions for runner and io manager methods, and adds them to @@ -717,7 +717,7 @@ class NurseryManager: async def __aenter__(self): self._scope = CancelScope() self._scope.__enter__() - self._nursery = Nursery(current_task(), self._scope) + self._nursery = Nursery._create(current_task(), self._scope) return self._nursery @enable_ki_protection @@ -761,7 +761,7 @@ def open_nursery(): return NurseryManager() -class Nursery: +class Nursery(metaclass=NoPublicConstructor): def __init__(self, parent_task, cancel_scope): self._parent_task = parent_task parent_task._child_nurseries.append(self) diff --git a/trio/_core/tests/test_run.py b/trio/_core/tests/test_run.py index 5e356e5f3b..16b56c00ec 100644 --- a/trio/_core/tests/test_run.py +++ b/trio/_core/tests/test_run.py @@ -2142,6 +2142,30 @@ async def inner(): _core.run(inner) +def test_Nursery_init(): + check_Nursery_error = pytest.raises( + TypeError, match='no public constructor available' + ) + + with check_Nursery_error: + _core._run.Nursery(None, None) + + +async def test_Nursery_private_init(): + # context manager creation should not raise + async with _core.open_nursery() as nursery: + assert False == nursery._closed + + +def test_Nursery_subclass(): + with pytest.raises( + TypeError, match='`Nursery` does not support subclassing' + ): + + class Subclass(_core._run.Nursery): + pass + + def test_Cancelled_init(): check_Cancelled_error = pytest.raises( TypeError, match='no public constructor available' From 15c19fd3909bff99ad36d4bef77323a48a2056fe Mon Sep 17 00:00:00 2001 From: benjotron Date: Sun, 9 Jun 2019 00:47:05 -0500 Subject: [PATCH 2/4] Export Nursery as trio.Nursery --- docs/source/history.rst | 2 +- docs/source/reference-core.rst | 2 +- trio/__init__.py | 3 ++- trio/_core/_run.py | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/source/history.rst b/docs/source/history.rst index 00e48139c7..a91f7bcbcd 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -440,7 +440,7 @@ Highlights :ref:`async-file-io` (`gh-20 `__) -* The new nursery :meth:`~The nursery interface.start` method makes it +* The new nursery :meth:`~Nursery.start` method makes it easy to perform controlled start-up of long-running tasks. For example, given an appropriate ``http_server_on_random_open_port`` function, you could write:: diff --git a/docs/source/reference-core.rst b/docs/source/reference-core.rst index 95a24781ab..d2ec471fef 100644 --- a/docs/source/reference-core.rst +++ b/docs/source/reference-core.rst @@ -928,7 +928,7 @@ Nursery objects provide the following interface: .. attribute:: TASK_STATUS_IGNORED - See :meth:`~The nursery interface.start`. + See :meth:`~Nursery.start`. Working with :exc:`MultiError`\s diff --git a/trio/__init__.py b/trio/__init__.py index df39b568b3..4a60467915 100644 --- a/trio/__init__.py +++ b/trio/__init__.py @@ -19,7 +19,8 @@ TrioInternalError, RunFinishedError, WouldBlock, Cancelled, BusyResourceError, ClosedResourceError, MultiError, run, open_nursery, CancelScope, open_cancel_scope, current_effective_deadline, - TASK_STATUS_IGNORED, current_time, BrokenResourceError, EndOfChannel + TASK_STATUS_IGNORED, current_time, BrokenResourceError, EndOfChannel, + Nursery ) from ._timeouts import ( diff --git a/trio/_core/_run.py b/trio/_core/_run.py index 8b654a90b3..4071e29d96 100644 --- a/trio/_core/_run.py +++ b/trio/_core/_run.py @@ -46,7 +46,7 @@ __all__ = [ "Task", "run", "open_nursery", "open_cancel_scope", "CancelScope", "checkpoint", "current_task", "current_effective_deadline", - "checkpoint_if_cancelled", "TASK_STATUS_IGNORED" + "checkpoint_if_cancelled", "TASK_STATUS_IGNORED", "Nursery" ] GLOBAL_RUN_CONTEXT = threading.local() From 4d2f7afb496a8e55d6b2381ae4cec7495a4f635c Mon Sep 17 00:00:00 2001 From: benjotron Date: Sun, 9 Jun 2019 16:29:58 -0500 Subject: [PATCH 3/4] Nursery docstring --- docs/source/reference-core.rst | 107 +------------------------------- newsfragments/1021.misc.rst | 7 ++- trio/_core/_run.py | 109 ++++++++++++++++++++++++++++++++- 3 files changed, 114 insertions(+), 109 deletions(-) diff --git a/docs/source/reference-core.rst b/docs/source/reference-core.rst index d2ec471fef..6f8c15ea55 100644 --- a/docs/source/reference-core.rst +++ b/docs/source/reference-core.rst @@ -819,112 +819,9 @@ The nursery API .. autofunction:: open_nursery :async-with: nursery -Nursery objects provide the following interface: - -.. interface:: The nursery interface - - .. method:: start_soon(async_fn, *args, name=None) - - Creates a new child task inside this nursery, and sets it up to - run ``await async_fn(*args)``. - - This and :meth:`start` are the two fundamental methods for - creating concurrent tasks in Trio. - - Note that this is *not* an async function and you don't use await - when calling it. It sets up the new task, but then returns - immediately, *before* it has a chance to run. The new task won’t - actually get a chance to do anything until some later point when - you execute a checkpoint and the scheduler decides to run it. - 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()`. - - 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 - nursery. - - The child task inherits its parent nursery's cancel scopes. - - :param async_fn: An async callable. - :param args: Positional arguments for ``async_fn``. If you want - to pass keyword arguments, use - :func:`functools.partial`. - :param name: The name for this task. Only used for - debugging/introspection - (e.g. ``repr(task_obj)``). If this isn't a string, - :meth:`start_soon` will try to make it one. A - common use case is if you're wrapping a function - before spawning a new task, you might pass the - original function as the ``name=`` to make - debugging easier. - :raises RuntimeError: If this nursery is no longer open - (i.e. its ``async with`` block has - exited). - - .. method:: start(async_fn, *args, name=None) - :async: - - Like :meth:`start_soon`, but blocks until the new task has - finished initializing itself, and optionally returns some - information from it. - - The ``async_fn`` must accept a ``task_status`` keyword argument, - and it must make sure that it (or someone) eventually calls - ``task_status.started()``. - - The conventional way to define ``async_fn`` is like:: - - async def async_fn(arg1, arg2, *, task_status=trio.TASK_STATUS_IGNORED): - ... - task_status.started() - ... - - :attr:`trio.TASK_STATUS_IGNORED` is a special global object with - a do-nothing ``started`` method. This way your function supports - being called either like ``await nursery.start(async_fn, arg1, - arg2)`` or directly like ``await async_fn(arg1, arg2)``, and - either way it can call ``task_status.started()`` without - worrying about which mode it's in. Defining your function like - this will make it obvious to readers that it supports being used - in both modes. - - Before the child calls ``task_status.started()``, it's - effectively run underneath the call to :meth:`start`: if it - raises an exception then that exception is reported by - :meth:`start`, and does *not* propagate out of the nursery. If - :meth:`start` is cancelled, then the child task is also - cancelled. - - When the child calls ``task_status.started()``, it's moved from - out from underneath :meth:`start` and into the given nursery. - - If the child task passes a value to - ``task_status.started(value)``, then :meth:`start` returns this - value. Otherwise it returns ``None``. - - .. attribute:: cancel_scope - - Creating a nursery also implicitly creates a cancellation scope, - which is exposed as the :attr:`cancel_scope` attribute. This is - used internally to implement the logic where if an error occurs - then ``__aexit__`` cancels all children, but you can use it for - 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.hazmat.Task` that opened this nursery. - - .. attribute:: child_tasks - - A :class:`frozenset` containing all the child - :class:`~trio.hazmat.Task` objects which are still running. +.. autoclass:: Nursery + :members: .. attribute:: TASK_STATUS_IGNORED diff --git a/newsfragments/1021.misc.rst b/newsfragments/1021.misc.rst index 526b21d0db..fe24e55968 100644 --- a/newsfragments/1021.misc.rst +++ b/newsfragments/1021.misc.rst @@ -1,3 +1,4 @@ -Any attempt to inherit from :class:`CancelScope` now raises TypeError. -(Trio has never been able to safely support subclassing here; -this change just makes it more obvious.) \ No newline at end of file +Any attempt to inherit from `CancelScope` or `Nursery` now raises +`TypeError`. (Trio has never been able to safely support subclassing +here; this change just makes it more obvious.) +Also exposed as public classes for type-checking, etc. diff --git a/trio/_core/_run.py b/trio/_core/_run.py index 4071e29d96..0b02ec585c 100644 --- a/trio/_core/_run.py +++ b/trio/_core/_run.py @@ -752,7 +752,7 @@ def __exit__(self): # pragma: no cover def open_nursery(): """Returns an async context manager which must be used to create a - new ``Nursery``. + new `Nursery`. It does not block on entry; on exit it blocks until all child tasks have exited. @@ -762,6 +762,27 @@ def open_nursery(): class Nursery(metaclass=NoPublicConstructor): + """A context which may be used to spawn (or cancel) child tasks. + + Not constructed directly, use `open_nursery` instead. + + The nursery will remain open until all child tasks have completed, + or until it is cancelled, at which point it will cancel all its + remaining child tasks and close. + + Nurseries ensure the absence of orphaned Tasks, since all running + tasks will belong to an open Nursery. + + Attributes: + cancel_scope: + Creating a nursery also implicitly creates a cancellation scope, + which is exposed as the :attr:`cancel_scope` attribute. This is + used internally to implement the logic where if an error occurs + then ``__aexit__`` cancels all children, but you can use it for + other things, e.g. if you want to explicitly cancel all children + in response to some external event. + """ + def __init__(self, parent_task, cancel_scope): self._parent_task = parent_task parent_task._child_nurseries.append(self) @@ -784,10 +805,13 @@ def __init__(self, parent_task, cancel_scope): @property def child_tasks(self): + """(`frozenset`): Contains all the child :class:`~trio.hazmat.Task` + objects which are still running.""" return frozenset(self._children) @property def parent_task(self): + "(`~trio.hazmat.Task`): The Task that opened this nursery." return self._parent_task def _add_exc(self, exc): @@ -841,9 +865,92 @@ def aborted(raise_cancel): return MultiError(self._pending_excs) def start_soon(self, async_fn, *args, name=None): + """ Creates a child task, scheduling ``await async_fn(*args)``. + + This and :meth:`start` are the two fundamental methods for + creating concurrent tasks in Trio. + + Note that this is *not* an async function and you don't use await + when calling it. It sets up the new task, but then returns + immediately, *before* it has a chance to run. The new task won’t + actually get a chance to do anything until some later point when + you execute a checkpoint and the scheduler decides to run it. + 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()`. + + 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 + nursery. + + The child task inherits its parent nursery's cancel scopes. + + Args: + async_fn: An async callable. + args: Positional arguments for ``async_fn``. If you want + to pass keyword arguments, use + :func:`functools.partial`. + name: The name for this task. Only used for + debugging/introspection + (e.g. ``repr(task_obj)``). If this isn't a string, + :meth:`start_soon` will try to make it one. A + common use case is if you're wrapping a function + before spawning a new task, you might pass the + original function as the ``name=`` to make + debugging easier. + + Returns: + True if successful, False otherwise. + + Raises: + RuntimeError: If this nursery is no longer open + (i.e. its ``async with`` block has + exited). + """ GLOBAL_RUN_CONTEXT.runner.spawn_impl(async_fn, args, self, name) async def start(self, async_fn, *args, name=None): + r""" Creates and initalizes a child task. + + Like :meth:`start_soon`, but blocks until the new task has + finished initializing itself, and optionally returns some + information from it. + + The ``async_fn`` must accept a ``task_status`` keyword argument, + and it must make sure that it (or someone) eventually calls + ``task_status.started()``. + + The conventional way to define ``async_fn`` is like:: + + async def async_fn(arg1, arg2, \*, task_status=trio.TASK_STATUS_IGNORED): + ... + task_status.started() + ... + + :attr:`trio.TASK_STATUS_IGNORED` is a special global object with + a do-nothing ``started`` method. This way your function supports + being called either like ``await nursery.start(async_fn, arg1, + arg2)`` or directly like ``await async_fn(arg1, arg2)``, and + either way it can call ``task_status.started()`` without + worrying about which mode it's in. Defining your function like + this will make it obvious to readers that it supports being used + in both modes. + + Before the child calls ``task_status.started()``, it's + effectively run underneath the call to :meth:`start`: if it + raises an exception then that exception is reported by + :meth:`start`, and does *not* propagate out of the nursery. If + :meth:`start` is cancelled, then the child task is also + cancelled. + + When the child calls ``task_status.started()``, it's moved from + out from underneath :meth:`start` and into the given nursery. + + If the child task passes a value to + ``task_status.started(value)``, then :meth:`start` returns this + value. Otherwise it returns ``None``. + """ if self._closed: raise RuntimeError("Nursery is closed to new arrivals") try: From 172723a47fbc34aca2ef058cb2e38fb136254ccf Mon Sep 17 00:00:00 2001 From: benjotron Date: Fri, 14 Jun 2019 07:45:58 -0500 Subject: [PATCH 4/4] Hiding Nursery constructor params from documentation --- docs/source/reference-core.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/reference-core.rst b/docs/source/reference-core.rst index 6f8c15ea55..4d993bb63c 100644 --- a/docs/source/reference-core.rst +++ b/docs/source/reference-core.rst @@ -820,7 +820,7 @@ The nursery API :async-with: nursery -.. autoclass:: Nursery +.. autoclass:: Nursery() :members: .. attribute:: TASK_STATUS_IGNORED