Skip to content

Commit 9a17132

Browse files
authoredApr 9, 2021
Added TaskGroup.start_soon() and changed spawn() back into a coroutine function (#254)
* Added TaskGroup.start_soon() and changed spawn() back into a coroutine function * Renamed BlockingPortal.spawn_task to start_task_soon One more step towards a trio-like API.
1 parent 2c0e648 commit 9a17132

28 files changed

+227
-174
lines changed
 

‎docs/cancellation.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ To accomplish this, open a new cancel scope with the ``shield=True`` argument::
6969
async def main():
7070
async with create_task_group() as tg:
7171
with CancelScope(shield=True) as scope:
72-
tg.spawn(external_task)
72+
tg.start_soon(external_task)
7373
tg.cancel_scope.cancel()
7474
print('Started sleeping in the host task')
7575
await sleep(1)
@@ -146,7 +146,7 @@ Therefore, do **NOT** do this::
146146

147147
async def some_generator():
148148
async with create_task_group() as tg:
149-
tg.spawn(foo)
149+
tg.start_soon(foo)
150150
yield
151151

152152
The problem with this code is that it violates structural concurrency: what happens if the spawned

‎docs/migration.rst

+21-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Migrating from AnyIO 2 to AnyIO 3
44
.. py:currentmodule:: anyio
55
66
AnyIO 3 changed some functions and methods in a way that needs some adaptation in your code.
7+
All deprecated functions and methods will be removed in AnyIO 4.
78

89
Asynchronous functions converted to synchronous
910
-----------------------------------------------
@@ -31,7 +32,6 @@ The following functions and methods were changed:
3132
* :meth:`MemoryObjectSendStream.send_nowait() <.streams.memory.MemoryObjectSendStream.send_nowait>`
3233
* :func:`open_signal_receiver`
3334
* :meth:`Semaphore.release`
34-
* :meth:`TaskGroup.spawn() <.abc.TaskGroup.spawn>`
3535

3636
When migrating to AnyIO 3, simply remove the ``await`` from each call to these.
3737

@@ -81,6 +81,26 @@ Example 2 – opening a cancel scope::
8181

8282
.. _trio: https://github.com/python-trio/trio
8383

84+
Starting tasks
85+
--------------
86+
87+
The :meth:`TaskGroup.spawn` coroutine method has been deprecated in favor of the synchronous
88+
method :meth:`TaskGroup.start_soon` (which mirrors ``start_soon()`` in trio's nurseries). If you're
89+
fully migrating to AnyIO 3, simply switch to calling the new method (and remove the ``await``).
90+
91+
The :meth:`BlockingPortal.spawn_task` method has also been renamed to
92+
:meth:`~BlockingPortal.start_task_soon`, so as to be consistent with task groups.
93+
94+
If your code needs to work with both AnyIO 2 and 3, you can keep using :meth:`~TaskGroup.spawn`
95+
(until AnyIO 4) and suppress the deprecation warning::
96+
97+
import warnings
98+
99+
async def foo():
100+
async with create_task_group() as tg:
101+
with warnings.catch_warnings():
102+
await tg.spawn(otherfunc)
103+
84104
Synchronization primitives
85105
--------------------------
86106

‎docs/signals.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ signal handler::
6060

6161
async def main():
6262
async with create_task_group() as tg:
63-
tg.spawn(signal_handler, tg.cancel_scope)
63+
tg.start_soon(signal_handler, tg.cancel_scope)
6464
... # proceed with starting the actual application logic
6565

6666
run(main)

‎docs/streams.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ Example::
6060
async def main():
6161
send_stream, receive_stream = create_memory_object_stream()
6262
async with create_task_group() as tg:
63-
tg.spawn(process_items, receive_stream)
63+
tg.start_soon(process_items, receive_stream)
6464
async with send_stream:
6565
for num in range(10):
6666
await send_stream.send(f'number {num}')

‎docs/synchronization.rst

+5-5
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Example::
2525
async def main():
2626
event = Event()
2727
async with create_task_group() as tg:
28-
tg.spawn(notify, event)
28+
tg.start_soon(notify, event)
2929
await event.wait()
3030
print('Received notification!')
3131

@@ -58,7 +58,7 @@ Example::
5858
semaphore = Semaphore(2)
5959
async with create_task_group() as tg:
6060
for num in range(10):
61-
tg.spawn(use_resource, num, semaphore)
61+
tg.start_soon(use_resource, num, semaphore)
6262

6363
run(main)
6464

@@ -84,7 +84,7 @@ Example::
8484
lock = Lock()
8585
async with create_task_group() as tg:
8686
for num in range(4):
87-
tg.spawn(use_resource, num, lock)
87+
tg.start_soon(use_resource, num, lock)
8888

8989
run(main)
9090

@@ -115,7 +115,7 @@ Example::
115115
condition = Condition()
116116
async with create_task_group() as tg:
117117
for tasknum in range(6):
118-
tg.spawn(listen, tasknum, condition)
118+
tg.start_soon(listen, tasknum, condition)
119119

120120
await sleep(1)
121121
async with condition:
@@ -153,7 +153,7 @@ Example::
153153
limiter = CapacityLimiter(2)
154154
async with create_task_group() as tg:
155155
for num in range(10):
156-
tg.spawn(use_resource, num, limiter)
156+
tg.start_soon(use_resource, num, limiter)
157157

158158
run(main)
159159

‎docs/tasks.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Here's a demonstration::
3030
async def main():
3131
async with create_task_group() as tg:
3232
for num in range(5):
33-
tg.spawn(sometask, num)
33+
tg.start_soon(sometask, num)
3434

3535
print('All tasks finished!')
3636

@@ -77,7 +77,7 @@ calling will :meth:`TaskGroup.start() <.abc.TaskGroup.start>` will be blocked un
7777
spawned task never calls it, then the :meth:`TaskGroup.start() <.abc.TaskGroup.start>` call will
7878
raise a ``RuntimeError``.
7979

80-
.. note:: Unlike :meth:`~.abc.TaskGroup.spawn`, :meth:`~.abc.TaskGroup.start` needs an ``await``.
80+
.. note:: Unlike :meth:`~.abc.TaskGroup.start_soon`, :meth:`~.abc.TaskGroup.start` needs an ``await``.
8181

8282
Handling multiple errors in a task group
8383
----------------------------------------

‎docs/threads.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ Spawning tasks from worker threads
8585
----------------------------------
8686

8787
When you need to spawn a task to be run in the background, you can do so using
88-
:meth:`~.BlockingPortal.spawn_task`::
88+
:meth:`~.BlockingPortal.start_task_soon`::
8989

9090
from concurrent.futures import as_completed
9191

@@ -100,7 +100,7 @@ When you need to spawn a task to be run in the background, you can do so using
100100

101101

102102
with start_blocking_portal() as portal:
103-
futures = [portal.spawn_task(long_running_task, i) for i in range(1, 5)]
103+
futures = [portal.start_task_soon(long_running_task, i) for i in range(1, 5)]
104104
for future in as_completed(futures):
105105
print(future.result())
106106

‎docs/versionhistory.rst

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
66
**UNRELEASED**
77

88
- Fixed ``Semaphore.acquire()`` raising ``WouldBlock`` when a race condition occurs
9+
- Changed ``TaskGroup.spawn()`` back into a coroutine function and added
10+
``TaskGroup.start_soon`` as the replacement
11+
- Renamed ``BlockingPortal.spawn_task`` into ``BlockingPortal.start_task_soon``
912

1013
**3.0.0rc3**
1114

‎src/anyio/_backends/_asyncio.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -623,9 +623,8 @@ def task_done(_task: asyncio.Task) -> None:
623623
self.cancel_scope._tasks.add(task)
624624
return task
625625

626-
def spawn(self, func: Callable[..., Coroutine], *args, name=None) -> DeprecatedAwaitable:
626+
def start_soon(self, func: Callable[..., Coroutine], *args, name=None) -> None:
627627
self._spawn(func, args, name)
628-
return DeprecatedAwaitable(self.spawn)
629628

630629
async def start(self, func: Callable[..., Coroutine], *args, name=None) -> None:
631630
future: asyncio.Future = asyncio.Future()
@@ -766,7 +765,7 @@ def __init__(self):
766765
def _spawn_task_from_thread(self, func: Callable, args: tuple, kwargs: Dict[str, Any],
767766
name, future: Future) -> None:
768767
run_sync_from_thread(
769-
partial(self._task_group.spawn, name=name), self._call_func, func, args, kwargs,
768+
partial(self._task_group.start_soon, name=name), self._call_func, func, args, kwargs,
770769
future, loop=self._loop)
771770

772771

‎src/anyio/_backends/_trio.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,11 @@ async def __aexit__(self, exc_type: Optional[Type[BaseException]],
132132
finally:
133133
self._active = False
134134

135-
def spawn(self, func: Callable, *args, name=None) -> DeprecatedAwaitable:
135+
def start_soon(self, func: Callable, *args, name=None) -> None:
136136
if not self._active:
137137
raise RuntimeError('This task group is not active; no new tasks can be spawned.')
138138

139139
self._nursery.start_soon(func, *args, name=name)
140-
return DeprecatedAwaitable(self.spawn)
141140

142141
async def start(self, func: Callable[..., Coroutine], *args, name=None):
143142
if not self._active:
@@ -171,7 +170,7 @@ def __init__(self):
171170
def _spawn_task_from_thread(self, func: Callable, args: tuple, kwargs: Dict[str, Any],
172171
name, future: Future) -> None:
173172
return trio.from_thread.run_sync(
174-
partial(self._task_group.spawn, name=name), self._call_func, func, args, kwargs,
173+
partial(self._task_group.start_soon, name=name), self._call_func, func, args, kwargs,
175174
future, trio_token=self._token)
176175

177176

‎src/anyio/_core/_sockets.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ async def try_connect(remote_host: str, event: Event):
173173
async with create_task_group() as tg:
174174
for i, (af, addr) in enumerate(target_addrs):
175175
event = Event()
176-
tg.spawn(try_connect, addr, event)
176+
tg.start_soon(try_connect, addr, event)
177177
with move_on_after(happy_eyeballs_delay):
178178
await event.wait()
179179

‎src/anyio/_core/_subprocesses.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ async def drain_stream(stream, index):
3737
try:
3838
async with create_task_group() as tg:
3939
if process.stdout:
40-
tg.spawn(drain_stream, process.stdout, 0)
40+
tg.start_soon(drain_stream, process.stdout, 0)
4141
if process.stderr:
42-
tg.spawn(drain_stream, process.stderr, 1)
42+
tg.start_soon(drain_stream, process.stderr, 1)
4343
if process.stdin and input:
4444
await process.stdin.send(input)
4545
await process.stdin.aclose()

‎src/anyio/abc/_sockets.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ async def serve(self, handler: Callable[[T_Stream], Any],
127127
async with context_manager:
128128
while True:
129129
stream = await self.accept()
130-
task_group.spawn(handler, stream)
130+
task_group.start_soon(handler, stream)
131131

132132

133133
class UDPSocket(UnreliableObjectStream[UDPPacketType], _SocketProvider):

‎src/anyio/abc/_tasks.py

+21-3
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
from abc import ABCMeta, abstractmethod
33
from types import TracebackType
44
from typing import Callable, Coroutine, Optional, Type, TypeVar
5-
6-
from anyio._core._compat import DeprecatedAwaitable
5+
from warnings import warn
76

87
if typing.TYPE_CHECKING:
98
from anyio._core._tasks import CancelScope
@@ -31,14 +30,33 @@ class TaskGroup(metaclass=ABCMeta):
3130

3231
cancel_scope: 'CancelScope'
3332

33+
async def spawn(self, func: Callable[..., Coroutine], *args, name=None) -> None:
34+
"""
35+
Deprecated alios for :meth:`start_soon`.
36+
37+
:param func: a coroutine function
38+
:param args: positional arguments to call the function with
39+
:param name: name of the task, for the purposes of introspection and debugging
40+
41+
.. deprecated:: 3.0
42+
Use :meth:`start_soon` instead. If your code needs AnyIO 2 compatibility, you
43+
can keep using this until AnyIO 4.
44+
45+
"""
46+
warn('spawn() is deprecated -- use start_soon() (without the "await") instead',
47+
DeprecationWarning)
48+
self.start_soon(func, *args, name=name)
49+
3450
@abstractmethod
35-
def spawn(self, func: Callable[..., Coroutine], *args, name=None) -> DeprecatedAwaitable:
51+
def start_soon(self, func: Callable[..., Coroutine], *args, name=None) -> None:
3652
"""
3753
Launch a new task in this task group.
3854
3955
:param func: a coroutine function
4056
:param args: positional arguments to call the function with
4157
:param name: name of the task, for the purposes of introspection and debugging
58+
59+
.. versionadded:: 3.0
4260
"""
4361

4462
@abstractmethod

‎src/anyio/from_thread.py

+24-5
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ async def run_async_cm(self):
9090

9191
def __enter__(self) -> T_co:
9292
self._enter_future = Future()
93-
self._exit_future = self._portal.spawn_task(self.run_async_cm)
93+
self._exit_future = self._portal.start_task_soon(self.run_async_cm)
9494
cm = self._enter_future.result()
9595
return cast(T_co, cm)
9696

@@ -219,9 +219,30 @@ def call(self, func, *args):
219219
the event loop thread
220220
221221
"""
222-
return self.spawn_task(func, *args).result()
222+
return self.start_task_soon(func, *args).result()
223223

224224
def spawn_task(self, func: Callable[..., Coroutine], *args, name=None) -> Future:
225+
"""
226+
Deprecated alias for :meth:`start_task_soon`.
227+
228+
:param func: the target coroutine function
229+
:param args: positional arguments passed to ``func``
230+
:param name: name of the task (will be coerced to a string if not ``None``)
231+
:return: a future that resolves with the return value of the callable if the task completes
232+
successfully, or with the exception raised in the task
233+
:raises RuntimeError: if the portal is not running or if this method is called from within
234+
the event loop thread
235+
236+
.. versionadded:: 2.1
237+
.. deprecated:: 3.0
238+
Use :meth:`start_task_soon` instead. If your code needs AnyIO 2 compatibility, you
239+
can keep using this until AnyIO 4.
240+
241+
"""
242+
warn('spawn_task() is deprecated -- use start_task_soon() instead', DeprecationWarning)
243+
return self.start_task_soon(func, *args, name=name)
244+
245+
def start_task_soon(self, func: Callable[..., Coroutine], *args, name=None) -> Future:
225246
"""
226247
Spawn a task in the portal's task group.
227248
@@ -236,9 +257,7 @@ def spawn_task(self, func: Callable[..., Coroutine], *args, name=None) -> Future
236257
:raises RuntimeError: if the portal is not running or if this method is called from within
237258
the event loop thread
238259
239-
.. versionadded:: 2.1
240-
.. versionchanged:: 3.0
241-
Added the ``name`` argument.
260+
.. versionadded:: 3.0
242261
243262
"""
244263
self._check_running()

‎src/anyio/streams/stapled.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ async def serve(self, handler: Callable[[T_Stream], Any],
109109

110110
async with create_task_group() as tg:
111111
for listener in self.listeners:
112-
tg.spawn(listener.serve, handler, task_group)
112+
tg.start_soon(listener.serve, handler, task_group)
113113

114114
async def aclose(self) -> None:
115115
for listener in self.listeners:

0 commit comments

Comments
 (0)
Please sign in to comment.