diff --git a/newsfragments/1599.deprecated.rst b/newsfragments/1599.deprecated.rst new file mode 100644 index 0000000000..2d0ecc6bf6 --- /dev/null +++ b/newsfragments/1599.deprecated.rst @@ -0,0 +1,19 @@ +In the current implementation of `nursery.start() +`, the new task starts running in a private +nursery created inside :meth:`trio.Nursery.start`, and only moves to +its "real" nursery once it calls ``task_status.started()``. Since Trio +doesn't hand out any references to that private temporary nursery, we +would like to be able to assume it only contains the single task being +started. However, the nursery object is still accessible using +debugging APIs such as +``trio.lowlevel.current_task().parent_nursery``, and it's possible +that some users have been using those APIs to start tasks in it. (If +it wasn't clear, we strongly recommend against this sort of thing, +because it makes private implementation details affect the behavior of +your code.) + +Until now, these sorts of shenanigans unintentionally *worked*: the +new tasks would move alongside the task being ``start()``\ed, and one +might not even realize they weren't getting the ``parent_nursery`` +they were expecting. Supporting this unintentional feature is becoming +burdensome, though, so we're deprecating it with no replacement. diff --git a/trio/_core/_run.py b/trio/_core/_run.py index 8b112051e5..3db4be8989 100644 --- a/trio/_core/_run.py +++ b/trio/_core/_run.py @@ -41,7 +41,7 @@ ) from ._thread_cache import start_thread_soon from .. import _core -from .._deprecate import deprecated +from .._deprecate import deprecated, warn_deprecated from .._util import Final, NoPublicConstructor, coroutine_or_error _NO_SEND = object() @@ -656,6 +656,20 @@ def __repr__(self): def started(self, value=None): if self._called_started: raise RuntimeError("called 'started' twice on the same task status") + if len(self._old_nursery._children) > 1: + # This can only happen if someone finds the old_nursery through + # debugging APIs and spawns more tasks into it. We don't think + # that's a reasonable thing to do, and supporting it makes other + # extensions to the nursery semantics much harder to implement. + # But it unintentionally worked in the past, so it gets a + # deprecation period. + warn_deprecated( + "starting additional tasks in the private old_nursery created " + "by Nursery.start()", + version="0.16.0", + issue=1599, + instead="a nursery that you created", + ) self._called_started = True self._value = value diff --git a/trio/_core/tests/test_run.py b/trio/_core/tests/test_run.py index 78b46b7adc..b818174d81 100644 --- a/trio/_core/tests/test_run.py +++ b/trio/_core/tests/test_run.py @@ -23,6 +23,7 @@ ) from ... import _core +from ..._deprecate import TrioDeprecationWarning from ..._threads import to_thread_run_sync from ..._timeouts import sleep, fail_after from ...testing import ( @@ -1883,6 +1884,16 @@ async def raise_keyerror_after_started(task_status=_core.TASK_STATUS_IGNORED,): await closed_nursery.start(sleep_then_start, 7) assert _core.current_time() == t0 + # started() raises a deprecation warning if there's more than one + # task in the old_nursery + async def starts_another_task_in_old_nursery(task_status): + _core.current_task().parent_nursery.start_soon(_core.checkpoint) + task_status.started() + + async with _core.open_nursery() as nursery: + with pytest.warns(TrioDeprecationWarning, match="private old_nursery"): + await nursery.start(starts_another_task_in_old_nursery) + async def test_task_nursery_stack(): task = _core.current_task()