From 1807bede3223bdba49dbd8f5e257c5d1a064ff9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 10 Nov 2024 14:24:47 +0200 Subject: [PATCH 01/13] Added support for asyncio eager task factories Fixes #764. --- .pre-commit-config.yaml | 4 ++-- docs/versionhistory.rst | 6 ++++++ src/anyio/_backends/_asyncio.py | 8 ++++++-- src/anyio/_core/_testing.py | 4 ++-- tests/test_sockets.py | 2 +- tests/test_taskgroups.py | 2 +- 6 files changed, 18 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 52a87347..e2da63d9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,14 +22,14 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.9 + rev: v0.7.1 hooks: - id: ruff args: [--fix, --show-fixes] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.2 + rev: v1.13.0 hooks: - id: mypy additional_dependencies: diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index 761be04f..922a3fe3 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -5,6 +5,12 @@ This library adheres to `Semantic Versioning 2.0 `_. **UNRELEASED** +- Added support for asyncio's eager task factories: + + * Updated the annotation of ``TaskInfo.coro`` to allow it to be ``None`` + * Updated ``TaskGroup`` to work with asyncio eager task factories + + (`#764 `_) - Fixed a misleading ``ValueError`` in the context of DNS failures (`#815 `_; PR by @graingert) diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index 0a69e7ac..ceebb1a0 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -347,8 +347,12 @@ def get_callable_name(func: Callable) -> str: def _task_started(task: asyncio.Task) -> bool: """Return ``True`` if the task has been started and has not finished.""" + # The task coro should never be None here, as we never add finished tasks to the + # task list + coro = task.get_coro() + assert coro is not None try: - return getcoroutinestate(task.get_coro()) in (CORO_RUNNING, CORO_SUSPENDED) + return getcoroutinestate(coro) in (CORO_RUNNING, CORO_SUSPENDED) except AttributeError: # task coro is async_genenerator_asend https://bugs.python.org/issue37771 raise Exception(f"Cannot determine if task {task} has started or not") from None @@ -842,7 +846,6 @@ def task_done(_task: asyncio.Task) -> None: name = get_callable_name(func) if name is None else str(name) task = create_task(coro, name=name) - task.add_done_callback(task_done) # Make the spawned task inherit the task group's cancel scope _task_states[task] = TaskState( @@ -850,6 +853,7 @@ def task_done(_task: asyncio.Task) -> None: ) self.cancel_scope._tasks.add(task) self._tasks.add(task) + task.add_done_callback(task_done) return task def start_soon( diff --git a/src/anyio/_core/_testing.py b/src/anyio/_core/_testing.py index 9e28b227..2ab9ea20 100644 --- a/src/anyio/_core/_testing.py +++ b/src/anyio/_core/_testing.py @@ -24,14 +24,14 @@ def __init__( id: int, parent_id: int | None, name: str | None, - coro: Generator[Any, Any, Any] | Awaitable[Any], + coro: Generator[Any, Any, Any] | Awaitable[Any] | None, ): func = get_current_task self._name = f"{func.__module__}.{func.__qualname__}" self.id: int = id self.parent_id: int | None = parent_id self.name: str | None = name - self.coro: Generator[Any, Any, Any] | Awaitable[Any] = coro + self.coro: Generator[Any, Any, Any] | Awaitable[Any] | None = coro def __eq__(self, other: object) -> bool: if isinstance(other, TaskInfo): diff --git a/tests/test_sockets.py b/tests/test_sockets.py index 0920f6ef..6c6a5f2f 100644 --- a/tests/test_sockets.py +++ b/tests/test_sockets.py @@ -144,7 +144,7 @@ def _identity(v: _T) -> _T: ) -@_ignore_win32_resource_warnings # type: ignore[operator] +@_ignore_win32_resource_warnings class TestTCPStream: @pytest.fixture def server_sock(self, family: AnyIPAddressFamily) -> Iterator[socket.socket]: diff --git a/tests/test_taskgroups.py b/tests/test_taskgroups.py index 78ef9983..e8918618 100644 --- a/tests/test_taskgroups.py +++ b/tests/test_taskgroups.py @@ -783,7 +783,7 @@ async def host_agen_fn() -> AsyncGenerator[None, None]: host_agen = host_agen_fn() try: loop = asyncio.get_running_loop() - await loop.create_task(host_agen.__anext__()) # type: ignore[arg-type] + await loop.create_task(host_agen.__anext__()) finally: await host_agen.aclose() From 2d557ce19b75129e2635c3e0fd4ac480593ff4a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 10 Nov 2024 15:17:42 +0200 Subject: [PATCH 02/13] Added test and made sure the eager task is cleaned up before anything else runs --- src/anyio/_backends/_asyncio.py | 8 +++++++- tests/test_taskgroups.py | 23 ++++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index ceebb1a0..aa212e32 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -853,7 +853,13 @@ def task_done(_task: asyncio.Task) -> None: ) self.cancel_scope._tasks.add(task) self._tasks.add(task) - task.add_done_callback(task_done) + + if task.done(): + # This can happen with eager task factories + task_done(task) + else: + task.add_done_callback(task_done) + return task def start_soon( diff --git a/tests/test_taskgroups.py b/tests/test_taskgroups.py index e8918618..014cd5fb 100644 --- a/tests/test_taskgroups.py +++ b/tests/test_taskgroups.py @@ -10,7 +10,8 @@ from typing import Any, NoReturn, cast import pytest -from exceptiongroup import ExceptionGroup, catch +from exceptiongroup import catch +from pytest import FixtureRequest from pytest_mock import MockerFixture import anyio @@ -1704,3 +1705,23 @@ async def typetest_optional_status( task_status: TaskStatus[int] = TASK_STATUS_IGNORED, ) -> None: task_status.started(1) + + +@pytest.mark.parametrize("anyio_backend", ["asyncio"]) +@pytest.mark.skipif( + sys.version_info < (3, 12), + reason="Eager task factories require Python 3.12", +) +async def test_eager_task_factory(request: FixtureRequest) -> None: + async def sync_coro() -> None: + pass + + loop = asyncio.get_running_loop() + old_task_factory = loop.get_task_factory() + loop.set_task_factory(asyncio.eager_task_factory) + request.addfinalizer(lambda: loop.set_task_factory(old_task_factory)) + + async with create_task_group() as tg: + tg.start_soon(sync_coro) + tg.cancel_scope.cancel() + await checkpoint() From 2891e20bfd364b29304f48dcf1b79515d627fa3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 10 Nov 2024 15:24:03 +0200 Subject: [PATCH 03/13] Swapped order of pytest marks to be consistent with other tests --- tests/test_taskgroups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_taskgroups.py b/tests/test_taskgroups.py index 014cd5fb..82b059fb 100644 --- a/tests/test_taskgroups.py +++ b/tests/test_taskgroups.py @@ -1707,11 +1707,11 @@ async def typetest_optional_status( task_status.started(1) -@pytest.mark.parametrize("anyio_backend", ["asyncio"]) @pytest.mark.skipif( sys.version_info < (3, 12), reason="Eager task factories require Python 3.12", ) +@pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_eager_task_factory(request: FixtureRequest) -> None: async def sync_coro() -> None: pass From 237d1a205004cc02bdcf3ad1aea5a61d4cebe7fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 10 Nov 2024 15:26:36 +0200 Subject: [PATCH 04/13] Removed unnecessary line of code --- tests/test_taskgroups.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_taskgroups.py b/tests/test_taskgroups.py index 82b059fb..9fa590c9 100644 --- a/tests/test_taskgroups.py +++ b/tests/test_taskgroups.py @@ -1724,4 +1724,3 @@ async def sync_coro() -> None: async with create_task_group() as tg: tg.start_soon(sync_coro) tg.cancel_scope.cancel() - await checkpoint() From 76a3594990cd216fe06c60699aa523f133a4d8b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Tue, 12 Nov 2024 01:28:36 +0200 Subject: [PATCH 05/13] mplemented custom mapping class for storing task states --- src/anyio/_backends/_asyncio.py | 66 ++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index aa212e32..fb07853b 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -28,6 +28,8 @@ Collection, Coroutine, Iterable, + Iterator, + MutableMapping, Sequence, ) from concurrent.futures import Future @@ -667,7 +669,49 @@ def __init__(self, parent_id: int | None, cancel_scope: CancelScope | None): self.cancel_scope = cancel_scope -_task_states: WeakKeyDictionary[asyncio.Task, TaskState] = WeakKeyDictionary() +class TaskStateStore(MutableMapping["Awaitable[Any] | asyncio.Task | None", TaskState]): + def __init__(self) -> None: + self._task_states = WeakKeyDictionary[asyncio.Task, TaskState]() + self._preliminary_task_states: dict[Awaitable[Any], TaskState] = {} + + def __getitem__(self, key: Awaitable[Any] | asyncio.Task | None, /) -> TaskState: + assert isinstance(key, asyncio.Task) + try: + return self._task_states[key] + except KeyError: + if coro := key.get_coro(): + if state := self._preliminary_task_states.get(coro): + return state + + raise KeyError(key) + + def __setitem__( + self, key: asyncio.Task | Awaitable[Any] | None, value: TaskState, / + ) -> None: + if isinstance(key, asyncio.Task): + self._task_states[key] = value + elif key is None: + raise ValueError("cannot insert None") + else: + self._preliminary_task_states[key] = value + + def __delitem__(self, key: asyncio.Task | Awaitable[Any] | None, /) -> None: + if isinstance(key, asyncio.Task): + del self._task_states[key] + elif key is None: + raise KeyError(key) + else: + del self._preliminary_task_states[key] + + def __len__(self) -> int: + return len(self._task_states) + len(self._preliminary_task_states) + + def __iter__(self) -> Iterator[Awaitable[Any] | asyncio.Task]: + yield from self._task_states + yield from self._preliminary_task_states + + +_task_states = TaskStateStore() # @@ -787,7 +831,7 @@ def _spawn( task_status_future: asyncio.Future | None = None, ) -> asyncio.Task: def task_done(_task: asyncio.Task) -> None: - task_state = _task_states[_task] + # task_state = _task_states[_task] assert task_state.cancel_scope is not None assert _task in task_state.cancel_scope._tasks task_state.cancel_scope._tasks.remove(_task) @@ -844,16 +888,22 @@ def task_done(_task: asyncio.Task) -> None: f"the return value ({coro!r}) is not a coroutine object" ) - name = get_callable_name(func) if name is None else str(name) - task = create_task(coro, name=name) - # Make the spawned task inherit the task group's cancel scope - _task_states[task] = TaskState( + _task_states[coro] = task_state = TaskState( parent_id=parent_id, cancel_scope=self.cancel_scope ) + name = get_callable_name(func) if name is None else str(name) + try: + task = create_task(coro, name=name) + except BaseException: + del _task_states[coro] + raise + self.cancel_scope._tasks.add(task) self._tasks.add(task) + del _task_states[coro] + _task_states[task] = task_state if task.done(): # This can happen with eager task factories task_done(task) @@ -2346,9 +2396,7 @@ def create_cancel_scope( @classmethod def current_effective_deadline(cls) -> float: try: - cancel_scope = _task_states[ - current_task() # type: ignore[index] - ].cancel_scope + cancel_scope = _task_states[current_task()].cancel_scope except KeyError: return math.inf From f25559c9d76816deb0395993fdc983f40b9d2078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Wed, 13 Nov 2024 02:22:53 +0200 Subject: [PATCH 06/13] Minor optimizations --- src/anyio/_backends/_asyncio.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index fb07853b..2649078d 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -895,15 +895,13 @@ def task_done(_task: asyncio.Task) -> None: name = get_callable_name(func) if name is None else str(name) try: task = create_task(coro, name=name) - except BaseException: + finally: del _task_states[coro] - raise + _task_states[task] = task_state self.cancel_scope._tasks.add(task) self._tasks.add(task) - del _task_states[coro] - _task_states[task] = task_state if task.done(): # This can happen with eager task factories task_done(task) From 4495f780de9b35b361860eb8a574b622c1f00897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 24 Nov 2024 00:41:56 +0200 Subject: [PATCH 07/13] Amended test_eager_task_factory to check that fetching the task state works --- tests/test_taskgroups.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_taskgroups.py b/tests/test_taskgroups.py index 9fa590c9..84101e47 100644 --- a/tests/test_taskgroups.py +++ b/tests/test_taskgroups.py @@ -1714,7 +1714,9 @@ async def typetest_optional_status( @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_eager_task_factory(request: FixtureRequest) -> None: async def sync_coro() -> None: - pass + # This should trigger fetching the task state + with CancelScope(): # noqa: ASYNC100 + pass loop = asyncio.get_running_loop() old_task_factory = loop.get_task_factory() From ae99439a6310814f8094622149a10ffe052faa40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Wed, 27 Nov 2024 00:06:45 +0200 Subject: [PATCH 08/13] Fixed KeyError when entering a cancel scope in an eager task --- src/anyio/_backends/_asyncio.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index 899e7955..06ff846c 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -415,8 +415,10 @@ def __enter__(self) -> CancelScope: self._parent_scope = task_state.cancel_scope task_state.cancel_scope = self if self._parent_scope is not None: + # If using an eager task factory, the parent scope may not even contain + # the host task self._parent_scope._child_scopes.add(self) - self._parent_scope._tasks.remove(host_task) + self._parent_scope._tasks.discard(host_task) self._timeout() self._active = True From 02b654e06628b2da7a99eac61a07968140b39626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sat, 30 Nov 2024 01:27:56 +0200 Subject: [PATCH 09/13] Simplified TaskStateStore to not store None keys --- src/anyio/_backends/_asyncio.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index 06ff846c..a6bce650 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -675,12 +675,12 @@ def __init__(self, parent_id: int | None, cancel_scope: CancelScope | None): self.cancel_scope = cancel_scope -class TaskStateStore(MutableMapping["Awaitable[Any] | asyncio.Task | None", TaskState]): +class TaskStateStore(MutableMapping["Awaitable[Any] | asyncio.Task", TaskState]): def __init__(self) -> None: self._task_states = WeakKeyDictionary[asyncio.Task, TaskState]() self._preliminary_task_states: dict[Awaitable[Any], TaskState] = {} - def __getitem__(self, key: Awaitable[Any] | asyncio.Task | None, /) -> TaskState: + def __getitem__(self, key: Awaitable[Any] | asyncio.Task, /) -> TaskState: assert isinstance(key, asyncio.Task) try: return self._task_states[key] @@ -692,20 +692,16 @@ def __getitem__(self, key: Awaitable[Any] | asyncio.Task | None, /) -> TaskState raise KeyError(key) def __setitem__( - self, key: asyncio.Task | Awaitable[Any] | None, value: TaskState, / + self, key: asyncio.Task | Awaitable[Any], value: TaskState, / ) -> None: if isinstance(key, asyncio.Task): self._task_states[key] = value - elif key is None: - raise ValueError("cannot insert None") else: self._preliminary_task_states[key] = value - def __delitem__(self, key: asyncio.Task | Awaitable[Any] | None, /) -> None: + def __delitem__(self, key: asyncio.Task | Awaitable[Any], /) -> None: if isinstance(key, asyncio.Task): del self._task_states[key] - elif key is None: - raise KeyError(key) else: del self._preliminary_task_states[key] @@ -2399,8 +2395,11 @@ def create_cancel_scope( @classmethod def current_effective_deadline(cls) -> float: + if (task := current_task()) is None: + return math.inf + try: - cancel_scope = _task_states[current_task()].cancel_scope + cancel_scope = _task_states[task].cancel_scope except KeyError: return math.inf From af86c3470a8ce125573c753f4eac328d2fb4dd8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sat, 30 Nov 2024 01:29:12 +0200 Subject: [PATCH 10/13] Reworded the changelog entry --- docs/versionhistory.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index 45293e8f..e97dff46 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -5,7 +5,7 @@ This library adheres to `Semantic Versioning 2.0 `_. **UNRELEASED** -- Added support for asyncio's eager task factories: +- Improved compatibility with asyncio's eager task factories: * Updated the annotation of ``TaskInfo.coro`` to allow it to be ``None`` * Updated ``TaskGroup`` to work with asyncio eager task factories From dfd79aeeb51f8ef43944023a2eea1c106a7dd15c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sat, 30 Nov 2024 13:25:21 +0200 Subject: [PATCH 11/13] Updated Ruff to the latest release --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e2da63d9..69e0878f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.1 + rev: v0.8.1 hooks: - id: ruff args: [--fix, --show-fixes] From f58bef46a2b285912abe2f0d35d5da89d96d4cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sat, 30 Nov 2024 13:50:29 +0200 Subject: [PATCH 12/13] Retracted the TaskInfo type annotation changes --- docs/versionhistory.rst | 6 +----- src/anyio/_backends/_asyncio.py | 4 +++- src/anyio/_core/_testing.py | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index e97dff46..ce4c4db0 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -5,11 +5,7 @@ This library adheres to `Semantic Versioning 2.0 `_. **UNRELEASED** -- Improved compatibility with asyncio's eager task factories: - - * Updated the annotation of ``TaskInfo.coro`` to allow it to be ``None`` - * Updated ``TaskGroup`` to work with asyncio eager task factories - +- Updated ``TaskGroup`` to work with asyncio's eager task factories (`#764 `_) - Fixed a misleading ``ValueError`` in the context of DNS failures (`#815 `_; PR by @graingert) diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index a6bce650..38b68f4d 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -2142,7 +2142,9 @@ def __init__(self, task: asyncio.Task): else: parent_id = task_state.parent_id - super().__init__(id(task), parent_id, task.get_name(), task.get_coro()) + coro = task.get_coro() + assert coro is not None, "created TaskInfo from a completed Task" + super().__init__(id(task), parent_id, task.get_name(), coro) self._task = weakref.ref(task) def has_pending_cancellation(self) -> bool: diff --git a/src/anyio/_core/_testing.py b/src/anyio/_core/_testing.py index 2ab9ea20..9e28b227 100644 --- a/src/anyio/_core/_testing.py +++ b/src/anyio/_core/_testing.py @@ -24,14 +24,14 @@ def __init__( id: int, parent_id: int | None, name: str | None, - coro: Generator[Any, Any, Any] | Awaitable[Any] | None, + coro: Generator[Any, Any, Any] | Awaitable[Any], ): func = get_current_task self._name = f"{func.__module__}.{func.__qualname__}" self.id: int = id self.parent_id: int | None = parent_id self.name: str | None = name - self.coro: Generator[Any, Any, Any] | Awaitable[Any] | None = coro + self.coro: Generator[Any, Any, Any] | Awaitable[Any] = coro def __eq__(self, other: object) -> bool: if isinstance(other, TaskInfo): From c7717070f6e5b1c8e22bbac1dd7c4f7b3ffec7a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 1 Dec 2024 00:11:39 +0200 Subject: [PATCH 13/13] Added note about eager task factory support being experimental --- src/anyio/abc/_tasks.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/anyio/abc/_tasks.py b/src/anyio/abc/_tasks.py index 88aecf38..f6e5c40c 100644 --- a/src/anyio/abc/_tasks.py +++ b/src/anyio/abc/_tasks.py @@ -40,6 +40,12 @@ class TaskGroup(metaclass=ABCMeta): :ivar cancel_scope: the cancel scope inherited by all child tasks :vartype cancel_scope: CancelScope + + .. note:: On asyncio, support for eager task factories is considered to be + **experimental**. In particular, they don't follow the usual semantics of new + tasks being scheduled on the next iteration of the event loop, and may thus + cause unexpected behavior in code that wasn't written with such semantics in + mind. """ cancel_scope: CancelScope