Skip to content

Commit

Permalink
Merge branch 'master' into fix-837
Browse files Browse the repository at this point in the history
  • Loading branch information
agronholm authored Jan 2, 2025
2 parents d5859a1 + 43e1f5f commit 04cf046
Show file tree
Hide file tree
Showing 17 changed files with 247 additions and 79 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/test-downstream.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ repos:
- id: trailing-whitespace

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.1
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:
Expand Down
10 changes: 10 additions & 0 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.

**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``
(`#836 <https://github.com/agronholm/anyio/pull/836>`_; PR by @graingert)
- Fixed ``AssertionError`` when using ``nest-asyncio``
(`#840 <https://github.com/agronholm/anyio/issues/840>`_)
- Fixed return type annotation of various context managers' ``__exit__`` method
(`#847 <https://github.com/agronholm/anyio/issues/847>`_; PR by @Enegg)
- Fixed cancellation edge case on asyncio where a task spawning another with
``TaskGroup.start()`` is not protected from external cancellation even when the
subtask has not yet called ``task_status.started()`` and is in a shielded cancel scope
Expand Down
7 changes: 2 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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"
Expand Down
41 changes: 21 additions & 20 deletions src/anyio/_backends/_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -2114,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
Expand Down Expand Up @@ -2446,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]],
Expand All @@ -2468,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)
Expand Down
8 changes: 4 additions & 4 deletions src/anyio/_backends/_trio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions src/anyio/_core/_asyncio_selector_thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
57 changes: 56 additions & 1 deletion src/anyio/_core/_fileio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 1 addition & 2 deletions src/anyio/_core/_synchronization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion src/anyio/_core/_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def __exit__(
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> bool | None:
) -> bool:
raise NotImplementedError


Expand Down
2 changes: 1 addition & 1 deletion src/anyio/to_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
pytest.mark.skip(reason="uvloop is missing shutdown_default_executor()")
)

pytest_plugins = ["pytester", "pytest_mock"]
pytest_plugins = ["pytester"]


@pytest.fixture(
Expand Down
18 changes: 8 additions & 10 deletions tests/streams/test_tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 04cf046

Please sign in to comment.