From 97f064295b65dc1d7a2ded6fd24ba5be46d4c4f4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 21:30:06 +0200 Subject: [PATCH 1/9] [pre-commit.ci] pre-commit autoupdate (#838) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.8.1 → v0.8.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.1...v0.8.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .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 69e0878f..19bbdc19 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.8.1 + rev: v0.8.2 hooks: - id: ruff args: [--fix, --show-fixes] From 3c5d4a6e8c21b0fcc75574b0b4b45575492a772b Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 16 Dec 2024 09:15:17 +0000 Subject: [PATCH 2/9] Use settings from Trio for Selector waker socketpair (#836) --- docs/versionhistory.rst | 7 +++++++ src/anyio/_core/_asyncio_selector_thread.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index 50745252..518aef88 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -3,6 +3,13 @@ Version history This library adheres to `Semantic Versioning 2.0 `_. +**UNRELEASED** + +- Configure ``SO_RCVBUF``, ``SO_SNDBUF`` and ``TCP_NODELAY`` on the selector + thread waker socket pair. This should improve the performance of ``wait_readable()`` + and ``wait_writable()`` when using the ``ProactorEventLoop`` + (`#836 `_; PR by @graingert) + **4.7.0** - Updated ``TaskGroup`` to work with asyncio's eager task factories diff --git a/src/anyio/_core/_asyncio_selector_thread.py b/src/anyio/_core/_asyncio_selector_thread.py index d98c3040..f4d18cf0 100644 --- a/src/anyio/_core/_asyncio_selector_thread.py +++ b/src/anyio/_core/_asyncio_selector_thread.py @@ -21,6 +21,23 @@ def __init__(self) -> None: self._send, self._receive = socket.socketpair() self._send.setblocking(False) self._receive.setblocking(False) + # This somewhat reduces the amount of memory wasted queueing up data + # for wakeups. With these settings, maximum number of 1-byte sends + # before getting BlockingIOError: + # Linux 4.8: 6 + # macOS (darwin 15.5): 1 + # Windows 10: 525347 + # Windows you're weird. (And on Windows setting SNDBUF to 0 makes send + # blocking, even on non-blocking sockets, so don't do that.) + self._receive.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1) + self._send.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1) + # On Windows this is a TCP socket so this might matter. On other + # platforms this fails b/c AF_UNIX sockets aren't actually TCP. + try: + self._send.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + except OSError: + pass + self._selector.register(self._receive, EVENT_READ) self._closed = False From d14f005c9649a42019f0d3fd4394d1fd66d54d18 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 00:06:48 +0200 Subject: [PATCH 3/9] [pre-commit.ci] pre-commit autoupdate (#842) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.8.2 → v0.8.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.2...v0.8.3) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .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 19bbdc19..5e65d60e 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.8.2 + rev: v0.8.3 hooks: - id: ruff args: [--fix, --show-fixes] From c518300593f2411196fd492d917269dcb6f7682b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Wed, 18 Dec 2024 00:07:30 +0200 Subject: [PATCH 4/9] Fixed AssertionError when using nest_asyncio (#841) This stems from the incorrect placement of `nest_asyncio.apply()`, as it should be called before `asyncio.run()`. Fixes #840. --- docs/versionhistory.rst | 2 ++ src/anyio/_backends/_asyncio.py | 32 +++++++++++++++++--------------- tests/test_taskgroups.py | 13 ++++++++++++- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index 518aef88..4901da4c 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -9,6 +9,8 @@ This library adheres to `Semantic Versioning 2.0 `_. thread waker socket pair. This should improve the performance of ``wait_readable()`` and ``wait_writable()`` when using the ``ProactorEventLoop`` (`#836 `_; PR by @graingert) +- Fixed ``AssertionError`` when using ``nest-asyncio`` + (`#840 `_) **4.7.0** diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index 0b7479d2..5a0aa936 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -677,40 +677,42 @@ def __init__(self, parent_id: int | None, cancel_scope: CancelScope | None): self.cancel_scope = cancel_scope -class TaskStateStore(MutableMapping["Awaitable[Any] | asyncio.Task", TaskState]): +class TaskStateStore( + MutableMapping["Coroutine[Any, Any, Any] | asyncio.Task", TaskState] +): def __init__(self) -> None: self._task_states = WeakKeyDictionary[asyncio.Task, TaskState]() - self._preliminary_task_states: dict[Awaitable[Any], TaskState] = {} + self._preliminary_task_states: dict[Coroutine[Any, Any, Any], TaskState] = {} - def __getitem__(self, key: Awaitable[Any] | asyncio.Task, /) -> TaskState: - assert isinstance(key, asyncio.Task) + def __getitem__(self, key: Coroutine[Any, Any, Any] | asyncio.Task, /) -> TaskState: + task = cast(asyncio.Task, key) try: - return self._task_states[key] + return self._task_states[task] except KeyError: - if coro := key.get_coro(): + if coro := task.get_coro(): if state := self._preliminary_task_states.get(coro): return state raise KeyError(key) def __setitem__( - self, key: asyncio.Task | Awaitable[Any], value: TaskState, / + self, key: asyncio.Task | Coroutine[Any, Any, Any], value: TaskState, / ) -> None: - if isinstance(key, asyncio.Task): - self._task_states[key] = value - else: + if isinstance(key, Coroutine): self._preliminary_task_states[key] = value - - def __delitem__(self, key: asyncio.Task | Awaitable[Any], /) -> None: - if isinstance(key, asyncio.Task): - del self._task_states[key] else: + self._task_states[key] = value + + def __delitem__(self, key: asyncio.Task | Coroutine[Any, Any, Any], /) -> None: + if isinstance(key, Coroutine): del self._preliminary_task_states[key] + else: + del self._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]: + def __iter__(self) -> Iterator[Coroutine[Any, Any, Any] | asyncio.Task]: yield from self._task_states yield from self._preliminary_task_states diff --git a/tests/test_taskgroups.py b/tests/test_taskgroups.py index 1f536940..d20565c1 100644 --- a/tests/test_taskgroups.py +++ b/tests/test_taskgroups.py @@ -11,7 +11,7 @@ import pytest from exceptiongroup import catch -from pytest import FixtureRequest +from pytest import FixtureRequest, MonkeyPatch from pytest_mock import MockerFixture import anyio @@ -1778,3 +1778,14 @@ async def sync_coro() -> None: async with create_task_group() as tg: tg.start_soon(sync_coro) tg.cancel_scope.cancel() + + +@pytest.mark.parametrize("anyio_backend", ["asyncio"]) +async def test_patched_asyncio_task(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setattr( + asyncio, + "Task", + asyncio.tasks._PyTask, # type: ignore[attr-defined] + ) + async with create_task_group() as tg: + tg.start_soon(sleep, 0) From 3f8c639d36d0ac01d341b25f6f99f1bc7aff6d12 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 24 Dec 2024 04:52:24 +0200 Subject: [PATCH 5/9] [pre-commit.ci] pre-commit autoupdate (#846) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.8.3 → v0.8.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.3...v0.8.4) - [github.com/pre-commit/mirrors-mypy: v1.13.0 → v1.14.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.13.0...v1.14.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5e65d60e..620a059d 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.8.3 + rev: v0.8.4 hooks: - id: ruff args: [--fix, --show-fixes] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.13.0 + rev: v1.14.0 hooks: - id: mypy additional_dependencies: From 2a105b2bc6ebb4d12b67b4c9c1777ff5cc54f42b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Thu, 2 Jan 2025 04:08:35 +0200 Subject: [PATCH 6/9] Updated downstream test workflow --- .github/workflows/test-downstream.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-downstream.yml b/.github/workflows/test-downstream.yml index 9a8b128f..5a95664b 100644 --- a/.github/workflows/test-downstream.yml +++ b/.github/workflows/test-downstream.yml @@ -74,7 +74,7 @@ jobs: with: python-version: "${{ matrix.python-version }}" - name: Setup uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v5 with: version: "0.4.15" enable-cache: true @@ -96,7 +96,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.12"] + python-version: ["3.9", "3.13"] steps: - uses: actions/checkout@v4 with: @@ -106,7 +106,7 @@ jobs: with: python-version: ${{ inputs.python-version }} - name: Install uv - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v5 with: version: "0.5.4" enable-cache: true From 9a792f3568f450ad97363d7bf0baa04f768ae291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Thu, 2 Jan 2025 15:39:57 +0200 Subject: [PATCH 7/9] Pruned unnecessary mypy options --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 73f6642b..9fed760f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,12 +93,9 @@ extend-select = [ [tool.mypy] python_version = "3.13" strict = true -ignore_missing_imports = true disallow_any_generics = false warn_return_any = false disallow_untyped_decorators = false -disallow_subclassing_any = false -show_error_codes = true [tool.pytest.ini_options] addopts = "-rsfE --tb=short --strict-config --strict-markers -p anyio -p no:asyncio -p no:trio" From e8730ae116aa924b0386fef15dcb3a7c15490283 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Thu, 2 Jan 2025 16:34:04 +0000 Subject: [PATCH 8/9] Added preliminary support for Python 3.14 (#813) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --------- Co-authored-by: Alex Grönholm Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- docs/versionhistory.rst | 2 ++ pyproject.toml | 4 +-- src/anyio/_core/_fileio.py | 57 +++++++++++++++++++++++++++++- tests/conftest.py | 2 +- tests/streams/test_tls.py | 18 +++++----- tests/test_eventloop.py | 12 ++++--- tests/test_fileio.py | 48 ++++++++++++++++++++++++++ tests/test_taskgroups.py | 71 ++++++++++++++++++++++++++------------ 9 files changed, 175 insertions(+), 41 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3c0badce..3c79d4dc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,7 +61,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", pypy-3.10] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", pypy-3.10] include: - os: macos-latest python-version: "3.9" diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index 4901da4c..245f9eee 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -5,6 +5,8 @@ This library adheres to `Semantic Versioning 2.0 `_. **UNRELEASED** +- Added support for the ``copy()``, ``copy_into()``, ``move()`` and ``move_into()`` + methods in ``anyio.Path``, available in Python 3.14 - Configure ``SO_RCVBUF``, ``SO_SNDBUF`` and ``TCP_NODELAY`` on the selector thread waker socket pair. This should improve the performance of ``wait_readable()`` and ``wait_writable()`` when using the ``ProactorEventLoop`` diff --git a/pyproject.toml b/pyproject.toml index 9fed760f..b8a9ea2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,12 +49,12 @@ test = [ "hypothesis >= 4.0", "psutil >= 5.9", "pytest >= 7.0", - "pytest-mock >= 3.6.1", "trustme", "truststore >= 0.9.1; python_version >= '3.10'", """\ uvloop >= 0.21; platform_python_implementation == 'CPython' \ - and platform_system != 'Windows'\ + and platform_system != 'Windows' \ + and python_version < '3.14'\ """ ] doc = [ diff --git a/src/anyio/_core/_fileio.py b/src/anyio/_core/_fileio.py index ef2930e4..4e34f2ad 100644 --- a/src/anyio/_core/_fileio.py +++ b/src/anyio/_core/_fileio.py @@ -3,7 +3,13 @@ import os import pathlib import sys -from collections.abc import AsyncIterator, Callable, Iterable, Iterator, Sequence +from collections.abc import ( + AsyncIterator, + Callable, + Iterable, + Iterator, + Sequence, +) from dataclasses import dataclass from functools import partial from os import PathLike @@ -220,11 +226,15 @@ class Path: Some methods may be unavailable or have limited functionality, based on the Python version: + * :meth:`~pathlib.Path.copy` (available on Python 3.14 or later) + * :meth:`~pathlib.Path.copy_into` (available on Python 3.14 or later) * :meth:`~pathlib.Path.from_uri` (available on Python 3.13 or later) * :meth:`~pathlib.Path.full_match` (available on Python 3.13 or later) * :meth:`~pathlib.Path.is_junction` (available on Python 3.12 or later) * :meth:`~pathlib.Path.match` (the ``case_sensitive`` paramater is only available on Python 3.13 or later) + * :meth:`~pathlib.Path.move` (available on Python 3.14 or later) + * :meth:`~pathlib.Path.move_into` (available on Python 3.14 or later) * :meth:`~pathlib.Path.relative_to` (the ``walk_up`` parameter is only available on Python 3.12 or later) * :meth:`~pathlib.Path.walk` (available on Python 3.12 or later) @@ -396,6 +406,51 @@ def match( def match(self, path_pattern: str) -> bool: return self._path.match(path_pattern) + if sys.version_info >= (3, 14): + + async def copy( + self, + target: str | os.PathLike[str], + *, + follow_symlinks: bool = True, + dirs_exist_ok: bool = False, + preserve_metadata: bool = False, + ) -> Path: + func = partial( + self._path.copy, + follow_symlinks=follow_symlinks, + dirs_exist_ok=dirs_exist_ok, + preserve_metadata=preserve_metadata, + ) + return Path(await to_thread.run_sync(func, target)) + + async def copy_into( + self, + target_dir: str | os.PathLike[str], + *, + follow_symlinks: bool = True, + dirs_exist_ok: bool = False, + preserve_metadata: bool = False, + ) -> Path: + func = partial( + self._path.copy_into, + follow_symlinks=follow_symlinks, + dirs_exist_ok=dirs_exist_ok, + preserve_metadata=preserve_metadata, + ) + return Path(await to_thread.run_sync(func, target_dir)) + + async def move(self, target: str | os.PathLike[str]) -> Path: + # Upstream does not handle anyio.Path properly as a PathLike + target = pathlib.Path(target) + return Path(await to_thread.run_sync(self._path.move, target)) + + async def move_into( + self, + target_dir: str | os.PathLike[str], + ) -> Path: + return Path(await to_thread.run_sync(self._path.move_into, target_dir)) + def is_relative_to(self, other: str | PathLike[str]) -> bool: try: self.relative_to(other) diff --git a/tests/conftest.py b/tests/conftest.py index 9d5acbfa..52998044 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,7 +26,7 @@ pytest.mark.skip(reason="uvloop is missing shutdown_default_executor()") ) -pytest_plugins = ["pytester", "pytest_mock"] +pytest_plugins = ["pytester"] @pytest.fixture( diff --git a/tests/streams/test_tls.py b/tests/streams/test_tls.py index d1ac2178..0b5eb95a 100644 --- a/tests/streams/test_tls.py +++ b/tests/streams/test_tls.py @@ -5,9 +5,9 @@ from contextlib import AbstractContextManager, ExitStack from threading import Thread from typing import NoReturn +from unittest import mock import pytest -from pytest_mock import MockerFixture from trustme import CA from anyio import ( @@ -375,18 +375,16 @@ def serve_sync() -> None: not hasattr(ssl, "OP_IGNORE_UNEXPECTED_EOF"), reason="The ssl module does not have the OP_IGNORE_UNEXPECTED_EOF attribute", ) - async def test_default_context_ignore_unexpected_eof_flag_off( - self, mocker: MockerFixture - ) -> None: + async def test_default_context_ignore_unexpected_eof_flag_off(self) -> None: send1, receive1 = create_memory_object_stream[bytes]() client_stream = StapledObjectStream(send1, receive1) - mocker.patch.object(TLSStream, "_call_sslobject_method") - tls_stream = await TLSStream.wrap(client_stream) - ssl_context = tls_stream.extra(TLSAttribute.ssl_object).context - assert not ssl_context.options & ssl.OP_IGNORE_UNEXPECTED_EOF + with mock.patch.object(TLSStream, "_call_sslobject_method"): + tls_stream = await TLSStream.wrap(client_stream) + ssl_context = tls_stream.extra(TLSAttribute.ssl_object).context + assert not ssl_context.options & ssl.OP_IGNORE_UNEXPECTED_EOF - send1.close() - receive1.close() + send1.close() + receive1.close() async def test_truststore_ssl( self, request: pytest.FixtureRequest, server_context: ssl.SSLContext diff --git a/tests/test_eventloop.py b/tests/test_eventloop.py index 7431acf7..68fa733f 100644 --- a/tests/test_eventloop.py +++ b/tests/test_eventloop.py @@ -3,11 +3,12 @@ import asyncio import math from asyncio import get_running_loop +from collections.abc import Generator +from unittest import mock from unittest.mock import AsyncMock import pytest from pytest import MonkeyPatch -from pytest_mock.plugin import MockerFixture from anyio import run, sleep_forever, sleep_until @@ -16,9 +17,12 @@ @pytest.fixture -def fake_sleep(mocker: MockerFixture) -> AsyncMock: - mocker.patch("anyio._core._eventloop.current_time", return_value=fake_current_time) - return mocker.patch("anyio._core._eventloop.sleep", AsyncMock()) +def fake_sleep() -> Generator[AsyncMock, None, None]: + with mock.patch( + "anyio._core._eventloop.current_time", return_value=fake_current_time + ): + with mock.patch("anyio._core._eventloop.sleep", AsyncMock()) as v: + yield v async def test_sleep_until(fake_sleep: AsyncMock) -> None: diff --git a/tests/test_fileio.py b/tests/test_fileio.py index f5d0183b..d7a769bd 100644 --- a/tests/test_fileio.py +++ b/tests/test_fileio.py @@ -326,6 +326,54 @@ async def test_is_symlink(self, tmp_path: pathlib.Path) -> None: def test_is_relative_to(self, arg: str, result: bool) -> None: assert Path("/xyz/abc/foo").is_relative_to(arg) == result + @pytest.mark.skipif( + sys.version_info < (3, 14), + reason="Path.copy() is only available on Python 3.14+", + ) + async def test_copy(self, tmp_path: pathlib.Path) -> None: + source_path = Path(tmp_path) / "source" + destination_path = Path(tmp_path) / "destination" + await source_path.write_text("hello") + result = await source_path.copy(destination_path) # type: ignore[attr-defined] + assert await result.read_text() == "hello" + + @pytest.mark.skipif( + sys.version_info < (3, 14), + reason="Path.copy() is only available on Python 3.14+", + ) + async def test_copy_into(self, tmp_path: pathlib.Path) -> None: + source_path = Path(tmp_path) / "source" + destination_path = Path(tmp_path) / "destination" + await destination_path.mkdir() + await source_path.write_text("hello") + result = await source_path.copy_into(destination_path) # type: ignore[attr-defined] + assert await result.read_text() == "hello" + + @pytest.mark.skipif( + sys.version_info < (3, 14), + reason="Path.copy() is only available on Python 3.14+", + ) + async def test_move(self, tmp_path: pathlib.Path) -> None: + source_path = Path(tmp_path) / "source" + destination_path = Path(tmp_path) / "destination" + await source_path.write_text("hello") + result = await source_path.move(destination_path) # type: ignore[attr-defined] + assert await result.read_text() == "hello" + assert not await source_path.exists() + + @pytest.mark.skipif( + sys.version_info < (3, 14), + reason="Path.copy() is only available on Python 3.14+", + ) + async def test_move_into(self, tmp_path: pathlib.Path) -> None: + source_path = Path(tmp_path) / "source" + destination_path = Path(tmp_path) / "destination" + await destination_path.mkdir() + await source_path.write_text("hello") + result = await source_path.move_into(destination_path) # type: ignore[attr-defined] + assert await result.read_text() == "hello" + assert not await source_path.exists() + async def test_glob(self, populated_tmpdir: pathlib.Path) -> None: all_paths = [] async for path in Path(populated_tmpdir).glob("**/*.txt"): diff --git a/tests/test_taskgroups.py b/tests/test_taskgroups.py index d20565c1..6410f5e3 100644 --- a/tests/test_taskgroups.py +++ b/tests/test_taskgroups.py @@ -8,11 +8,11 @@ from asyncio import CancelledError from collections.abc import AsyncGenerator, Coroutine, Generator from typing import Any, NoReturn, cast +from unittest import mock import pytest from exceptiongroup import catch from pytest import FixtureRequest, MonkeyPatch -from pytest_mock import MockerFixture import anyio from anyio import ( @@ -262,31 +262,43 @@ async def taskfunc() -> None: @pytest.mark.parametrize("anyio_backend", ["asyncio"]) -async def test_cancel_with_nested_task_groups(mocker: MockerFixture) -> None: +async def test_cancel_with_nested_task_groups() -> None: """Regression test for #695.""" async def shield_task() -> None: with CancelScope(shield=True) as scope: - shielded_cancel_spy = mocker.spy(scope, "_deliver_cancellation") - await sleep(0.5) + with mock.patch.object( + scope, + "_deliver_cancellation", + wraps=getattr(scope, "_deliver_cancellation"), + ) as shielded_cancel_spy: + await sleep(0.5) - assert len(outer_cancel_spy.call_args_list) < 10 - shielded_cancel_spy.assert_not_called() + assert len(outer_cancel_spy.call_args_list) < 10 + shielded_cancel_spy.assert_not_called() async def middle_task() -> None: try: async with create_task_group() as tg: - middle_cancel_spy = mocker.spy(tg.cancel_scope, "_deliver_cancellation") - tg.start_soon(shield_task, name="shield task") + with mock.patch.object( + tg.cancel_scope, + "_deliver_cancellation", + wraps=getattr(tg.cancel_scope, "_deliver_cancellation"), + ) as middle_cancel_spy: + tg.start_soon(shield_task, name="shield task") finally: assert len(middle_cancel_spy.call_args_list) < 10 assert len(outer_cancel_spy.call_args_list) < 10 async with create_task_group() as tg: - outer_cancel_spy = mocker.spy(tg.cancel_scope, "_deliver_cancellation") - tg.start_soon(middle_task, name="middle task") - await wait_all_tasks_blocked() - tg.cancel_scope.cancel() + with mock.patch.object( + tg.cancel_scope, + "_deliver_cancellation", + wraps=getattr(tg.cancel_scope, "_deliver_cancellation"), + ) as outer_cancel_spy: + tg.start_soon(middle_task, name="middle task") + await wait_all_tasks_blocked() + tg.cancel_scope.cancel() assert len(outer_cancel_spy.call_args_list) < 10 @@ -1602,14 +1614,29 @@ async def in_task_group(task_status: TaskStatus[None]) -> None: assert not tg.cancel_scope.cancel_called -if sys.version_info <= (3, 11): +if sys.version_info >= (3, 14): - def no_other_refs() -> list[object]: - return [sys._getframe(1)] -else: + async def no_other_refs() -> list[object]: + frame = sys._getframe(1) + coro = get_current_task().coro - def no_other_refs() -> list[object]: + async def get_coro_for_frame(*, task_status: TaskStatus[object]) -> None: + my_coro = coro + while my_coro.cr_frame is not frame: + my_coro = my_coro.cr_await + task_status.started(my_coro) + + async with create_task_group() as tg: + return [await tg.start(get_coro_for_frame)] + +elif sys.version_info >= (3, 11): + + async def no_other_refs() -> list[object]: return [] +else: + + async def no_other_refs() -> list[object]: + return [sys._getframe(1)] @pytest.mark.skipif( @@ -1640,7 +1667,7 @@ class _Done(Exception): exc = e assert exc is not None - assert gc.get_referrers(exc) == no_other_refs() + assert gc.get_referrers(exc) == await no_other_refs() async def test_exception_refcycles_errors(self) -> None: """Test that TaskGroup deletes self._exceptions, and __aexit__ args""" @@ -1657,7 +1684,7 @@ class _Done(Exception): exc = excs.exceptions[0] assert isinstance(exc, _Done) - assert gc.get_referrers(exc) == no_other_refs() + assert gc.get_referrers(exc) == await no_other_refs() async def test_exception_refcycles_parent_task(self) -> None: """Test that TaskGroup's cancel_scope deletes self._host_task""" @@ -1678,7 +1705,7 @@ async def coro_fn() -> None: exc = excs.exceptions[0].exceptions[0] assert isinstance(exc, _Done) - assert gc.get_referrers(exc) == no_other_refs() + assert gc.get_referrers(exc) == await no_other_refs() async def test_exception_refcycles_propagate_cancellation_error(self) -> None: """Test that TaskGroup deletes cancelled_exc""" @@ -1695,7 +1722,7 @@ async def test_exception_refcycles_propagate_cancellation_error(self) -> None: raise assert isinstance(exc, get_cancelled_exc_class()) - assert gc.get_referrers(exc) == no_other_refs() + assert gc.get_referrers(exc) == await no_other_refs() async def test_exception_refcycles_base_error(self) -> None: """ @@ -1718,7 +1745,7 @@ class MyKeyboardInterrupt(KeyboardInterrupt): exc = excs.exceptions[0] assert isinstance(exc, MyKeyboardInterrupt) - assert gc.get_referrers(exc) == no_other_refs() + assert gc.get_referrers(exc) == await no_other_refs() class TestTaskStatusTyping: From 43e1f5fd133b2f53993d606af207b06a0d09fecc Mon Sep 17 00:00:00 2001 From: Eneg <42005170+Enegg@users.noreply.github.com> Date: Thu, 2 Jan 2025 19:21:38 +0100 Subject: [PATCH 9/9] Fixed `__exit__()` return type of various context managers (#849) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --------- Co-authored-by: Alex Grönholm --- docs/versionhistory.rst | 2 ++ src/anyio/_backends/_asyncio.py | 9 ++++----- src/anyio/_backends/_trio.py | 8 ++++---- src/anyio/_core/_synchronization.py | 3 +-- src/anyio/_core/_tasks.py | 2 +- src/anyio/to_process.py | 2 +- tests/test_taskgroups.py | 3 ++- 7 files changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index 245f9eee..35fd7b8a 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -13,6 +13,8 @@ This library adheres to `Semantic Versioning 2.0 `_. (`#836 `_; PR by @graingert) - Fixed ``AssertionError`` when using ``nest-asyncio`` (`#840 `_) +- Fixed return type annotation of various context managers' ``__exit__`` method + (`#847 `_; PR by @Enegg) **4.7.0** diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index 5a0aa936..11582529 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -449,7 +449,7 @@ def __exit__( exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, - ) -> bool | None: + ) -> bool: del exc_tb if not self._active: @@ -2116,10 +2116,9 @@ def __exit__( exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, - ) -> bool | None: + ) -> None: for sig in self._handled_signals: self._loop.remove_signal_handler(sig) - return None def __aiter__(self) -> _SignalReceiver: return self @@ -2448,7 +2447,7 @@ def create_capacity_limiter(cls, total_tokens: float) -> abc.CapacityLimiter: return CapacityLimiter(total_tokens) @classmethod - async def run_sync_in_worker_thread( + async def run_sync_in_worker_thread( # type: ignore[return] cls, func: Callable[[Unpack[PosArgsT]], T_Retval], args: tuple[Unpack[PosArgsT]], @@ -2470,7 +2469,7 @@ async def run_sync_in_worker_thread( async with limiter or cls.current_default_thread_limiter(): with CancelScope(shield=not abandon_on_cancel) as scope: - future: asyncio.Future = asyncio.Future() + future = asyncio.Future[T_Retval]() root_task = find_root_task() if not idle_workers: worker = WorkerThread(root_task, workers, idle_workers) diff --git a/src/anyio/_backends/_trio.py b/src/anyio/_backends/_trio.py index 70a0a605..32ae8ace 100644 --- a/src/anyio/_backends/_trio.py +++ b/src/anyio/_backends/_trio.py @@ -132,8 +132,7 @@ def __exit__( exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, - ) -> bool | None: - # https://github.com/python-trio/trio-typing/pull/79 + ) -> bool: return self.__original.__exit__(exc_type, exc_val, exc_tb) def cancel(self) -> None: @@ -186,9 +185,10 @@ async def __aexit__( exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, - ) -> bool | None: + ) -> bool: try: - return await self._nursery_manager.__aexit__(exc_type, exc_val, exc_tb) + # trio.Nursery.__exit__ returns bool; .open_nursery has wrong type + return await self._nursery_manager.__aexit__(exc_type, exc_val, exc_tb) # type: ignore[return-value] except BaseExceptionGroup as exc: if not exc.split(trio.Cancelled)[1]: raise trio.Cancelled._create() from exc diff --git a/src/anyio/_core/_synchronization.py b/src/anyio/_core/_synchronization.py index 7878ba66..a6331328 100644 --- a/src/anyio/_core/_synchronization.py +++ b/src/anyio/_core/_synchronization.py @@ -728,6 +728,5 @@ def __exit__( exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, - ) -> bool | None: + ) -> None: self._guarded = False - return None diff --git a/src/anyio/_core/_tasks.py b/src/anyio/_core/_tasks.py index 2f21ea20..fe490151 100644 --- a/src/anyio/_core/_tasks.py +++ b/src/anyio/_core/_tasks.py @@ -88,7 +88,7 @@ def __exit__( exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, - ) -> bool | None: + ) -> bool: raise NotImplementedError diff --git a/src/anyio/to_process.py b/src/anyio/to_process.py index 5050dee2..495de2ae 100644 --- a/src/anyio/to_process.py +++ b/src/anyio/to_process.py @@ -35,7 +35,7 @@ _default_process_limiter: RunVar[CapacityLimiter] = RunVar("_default_process_limiter") -async def run_sync( +async def run_sync( # type: ignore[return] func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT], cancellable: bool = False, diff --git a/tests/test_taskgroups.py b/tests/test_taskgroups.py index 6410f5e3..1c1a654c 100644 --- a/tests/test_taskgroups.py +++ b/tests/test_taskgroups.py @@ -865,7 +865,8 @@ async def task(task_status: TaskStatus) -> NoReturn: completed = True scope.shield = False await sleep(1) - pytest.fail("Execution should not reach this point") + + pytest.fail("Execution should not reach this point") async with create_task_group() as tg: await tg.start(task)