diff options
| author | robot-piglet <[email protected]> | 2024-12-20 10:30:04 +0300 |
|---|---|---|
| committer | robot-piglet <[email protected]> | 2024-12-20 10:41:35 +0300 |
| commit | a7401f51adbf01f184068b9ac4f724479dc802e1 (patch) | |
| tree | f9a6d4ee87ff62cea290de049422ce89ea5a1c87 | |
| parent | f2be671abaceb9f7db98d5c7f1d603a80d51e393 (diff) | |
Intermediate changes
commit_hash:3a98ffbcfd1f534caf53d550524075486d6472e3
18 files changed, 503 insertions, 181 deletions
diff --git a/contrib/python/anyio/.dist-info/METADATA b/contrib/python/anyio/.dist-info/METADATA index 10d7aafc777..5d0f7c91301 100644 --- a/contrib/python/anyio/.dist-info/METADATA +++ b/contrib/python/anyio/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: anyio -Version: 4.6.2.post1 +Version: 4.7.0 Summary: High level compatibility layer for multiple asynchronous event loop implementations Author-email: Alex Grönholm <[email protected]> License: MIT @@ -23,28 +23,28 @@ Classifier: Programming Language :: Python :: 3.13 Requires-Python: >=3.9 Description-Content-Type: text/x-rst License-File: LICENSE -Requires-Dist: idna >=2.8 -Requires-Dist: sniffio >=1.1 -Requires-Dist: exceptiongroup >=1.0.2 ; python_version < "3.11" -Requires-Dist: typing-extensions >=4.1 ; python_version < "3.11" -Provides-Extra: doc -Requires-Dist: packaging ; extra == 'doc' -Requires-Dist: Sphinx ~=7.4 ; extra == 'doc' -Requires-Dist: sphinx-rtd-theme ; extra == 'doc' -Requires-Dist: sphinx-autodoc-typehints >=1.2.0 ; extra == 'doc' -Provides-Extra: test -Requires-Dist: anyio[trio] ; extra == 'test' -Requires-Dist: coverage[toml] >=7 ; extra == 'test' -Requires-Dist: exceptiongroup >=1.2.0 ; extra == 'test' -Requires-Dist: hypothesis >=4.0 ; extra == 'test' -Requires-Dist: psutil >=5.9 ; extra == 'test' -Requires-Dist: pytest >=7.0 ; extra == 'test' -Requires-Dist: pytest-mock >=3.6.1 ; extra == 'test' -Requires-Dist: trustme ; extra == 'test' -Requires-Dist: uvloop >=0.21.0b1 ; (platform_python_implementation == "CPython" and platform_system != "Windows") and extra == 'test' -Requires-Dist: truststore >=0.9.1 ; (python_version >= "3.10") and extra == 'test' +Requires-Dist: exceptiongroup>=1.0.2; python_version < "3.11" +Requires-Dist: idna>=2.8 +Requires-Dist: sniffio>=1.1 +Requires-Dist: typing_extensions>=4.5; python_version < "3.13" Provides-Extra: trio -Requires-Dist: trio >=0.26.1 ; extra == 'trio' +Requires-Dist: trio>=0.26.1; extra == "trio" +Provides-Extra: test +Requires-Dist: anyio[trio]; extra == "test" +Requires-Dist: coverage[toml]>=7; extra == "test" +Requires-Dist: exceptiongroup>=1.2.0; extra == "test" +Requires-Dist: hypothesis>=4.0; extra == "test" +Requires-Dist: psutil>=5.9; extra == "test" +Requires-Dist: pytest>=7.0; extra == "test" +Requires-Dist: pytest-mock>=3.6.1; extra == "test" +Requires-Dist: trustme; extra == "test" +Requires-Dist: truststore>=0.9.1; python_version >= "3.10" and extra == "test" +Requires-Dist: uvloop>=0.21; (platform_python_implementation == "CPython" and platform_system != "Windows") and extra == "test" +Provides-Extra: doc +Requires-Dist: packaging; extra == "doc" +Requires-Dist: Sphinx~=7.4; extra == "doc" +Requires-Dist: sphinx_rtd_theme; extra == "doc" +Requires-Dist: sphinx-autodoc-typehints>=1.2.0; extra == "doc" .. image:: https://github.com/agronholm/anyio/actions/workflows/test.yml/badge.svg :target: https://github.com/agronholm/anyio/actions/workflows/test.yml diff --git a/contrib/python/anyio/anyio/__init__.py b/contrib/python/anyio/anyio/__init__.py index fd9fe06bcfc..0738e595830 100644 --- a/contrib/python/anyio/anyio/__init__.py +++ b/contrib/python/anyio/anyio/__init__.py @@ -34,8 +34,10 @@ from ._core._sockets import create_unix_datagram_socket as create_unix_datagram_ from ._core._sockets import create_unix_listener as create_unix_listener from ._core._sockets import getaddrinfo as getaddrinfo from ._core._sockets import getnameinfo as getnameinfo +from ._core._sockets import wait_readable as wait_readable from ._core._sockets import wait_socket_readable as wait_socket_readable from ._core._sockets import wait_socket_writable as wait_socket_writable +from ._core._sockets import wait_writable as wait_writable from ._core._streams import create_memory_object_stream as create_memory_object_stream from ._core._subprocesses import open_process as open_process from ._core._subprocesses import run_process as run_process diff --git a/contrib/python/anyio/anyio/_backends/_asyncio.py b/contrib/python/anyio/anyio/_backends/_asyncio.py index 0a69e7ac610..0b7479d2649 100644 --- a/contrib/python/anyio/anyio/_backends/_asyncio.py +++ b/contrib/python/anyio/anyio/_backends/_asyncio.py @@ -28,6 +28,8 @@ from collections.abc import ( Collection, Coroutine, Iterable, + Iterator, + MutableMapping, Sequence, ) from concurrent.futures import Future @@ -50,6 +52,7 @@ from threading import Thread from types import TracebackType from typing import ( IO, + TYPE_CHECKING, Any, Optional, TypeVar, @@ -99,6 +102,11 @@ from ..abc._eventloop import StrOrBytesPath from ..lowlevel import RunVar from ..streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream +if TYPE_CHECKING: + from _typeshed import FileDescriptorLike +else: + FileDescriptorLike = object + if sys.version_info >= (3, 10): from typing import ParamSpec else: @@ -347,8 +355,12 @@ _run_vars: WeakKeyDictionary[asyncio.AbstractEventLoop, Any] = WeakKeyDictionary 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 @@ -360,11 +372,22 @@ def _task_started(task: asyncio.Task) -> bool: def is_anyio_cancellation(exc: CancelledError) -> bool: - return ( - bool(exc.args) - and isinstance(exc.args[0], str) - and exc.args[0].startswith("Cancelled by cancel scope ") - ) + # Sometimes third party frameworks catch a CancelledError and raise a new one, so as + # a workaround we have to look at the previous ones in __context__ too for a + # matching cancel message + while True: + if ( + exc.args + and isinstance(exc.args[0], str) + and exc.args[0].startswith("Cancelled by cancel scope ") + ): + return True + + if isinstance(exc.__context__, CancelledError): + exc = exc.__context__ + continue + + return False class CancelScope(BaseCancelScope): @@ -385,8 +408,10 @@ class CancelScope(BaseCancelScope): self._cancel_handle: asyncio.Handle | None = None self._tasks: set[asyncio.Task] = set() self._host_task: asyncio.Task | None = None - self._cancel_calls: int = 0 - self._cancelling: int | None = None + if sys.version_info >= (3, 11): + self._pending_uncancellations: int | None = 0 + else: + self._pending_uncancellations = None def __enter__(self) -> CancelScope: if self._active: @@ -405,13 +430,13 @@ class CancelScope(BaseCancelScope): 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 - if sys.version_info >= (3, 11): - self._cancelling = self._host_task.cancelling() # Start cancelling the host task if the scope was cancelled before entering if self._cancel_called: @@ -456,30 +481,41 @@ class CancelScope(BaseCancelScope): host_task_state.cancel_scope = self._parent_scope - # Undo all cancellations done by this scope - if self._cancelling is not None: - while self._cancel_calls: - self._cancel_calls -= 1 - if self._host_task.uncancel() <= self._cancelling: - break + # Restart the cancellation effort in the closest visible, cancelled parent + # scope if necessary + self._restart_cancellation_in_parent() # We only swallow the exception iff it was an AnyIO CancelledError, either # directly as exc_val or inside an exception group and there are no cancelled # parent cancel scopes visible to us here - not_swallowed_exceptions = 0 - swallow_exception = False - if exc_val is not None: - for exc in iterate_exceptions(exc_val): - if self._cancel_called and isinstance(exc, CancelledError): - if not (swallow_exception := self._uncancel(exc)): - not_swallowed_exceptions += 1 - else: - not_swallowed_exceptions += 1 + if self._cancel_called and not self._parent_cancellation_is_visible_to_us: + # For each level-cancel() call made on the host task, call uncancel() + while self._pending_uncancellations: + self._host_task.uncancel() + self._pending_uncancellations -= 1 - # Restart the cancellation effort in the closest visible, cancelled parent - # scope if necessary - self._restart_cancellation_in_parent() - return swallow_exception and not not_swallowed_exceptions + # Update cancelled_caught and check for exceptions we must not swallow + cannot_swallow_exc_val = False + if exc_val is not None: + for exc in iterate_exceptions(exc_val): + if isinstance(exc, CancelledError) and is_anyio_cancellation( + exc + ): + self._cancelled_caught = True + else: + cannot_swallow_exc_val = True + + return self._cancelled_caught and not cannot_swallow_exc_val + else: + if self._pending_uncancellations: + assert self._parent_scope is not None + assert self._parent_scope._pending_uncancellations is not None + self._parent_scope._pending_uncancellations += ( + self._pending_uncancellations + ) + self._pending_uncancellations = 0 + + return False finally: self._host_task = None del exc_val @@ -506,31 +542,6 @@ class CancelScope(BaseCancelScope): and self._parent_scope._effectively_cancelled ) - def _uncancel(self, cancelled_exc: CancelledError) -> bool: - if self._host_task is None: - self._cancel_calls = 0 - return True - - while True: - if is_anyio_cancellation(cancelled_exc): - # Only swallow the cancellation exception if it's an AnyIO cancel - # exception and there are no other cancel scopes down the line pending - # cancellation - self._cancelled_caught = ( - self._effectively_cancelled - and not self._parent_cancellation_is_visible_to_us - ) - return self._cancelled_caught - - # Sometimes third party frameworks catch a CancelledError and raise a new - # one, so as a workaround we have to look at the previous ones in - # __context__ too for a matching cancel message - if isinstance(cancelled_exc.__context__, CancelledError): - cancelled_exc = cancelled_exc.__context__ - continue - - return False - def _timeout(self) -> None: if self._deadline != math.inf: loop = get_running_loop() @@ -562,8 +573,11 @@ class CancelScope(BaseCancelScope): waiter = task._fut_waiter # type: ignore[attr-defined] if not isinstance(waiter, asyncio.Future) or not waiter.done(): task.cancel(f"Cancelled by cancel scope {id(origin):x}") - if task is origin._host_task: - origin._cancel_calls += 1 + if ( + task is origin._host_task + and origin._pending_uncancellations is not None + ): + origin._pending_uncancellations += 1 # Deliver cancellation to child scopes that aren't shielded or running their own # cancellation callbacks @@ -663,7 +677,45 @@ class TaskState: self.cancel_scope = cancel_scope -_task_states: WeakKeyDictionary[asyncio.Task, TaskState] = WeakKeyDictionary() +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, /) -> 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], value: TaskState, / + ) -> None: + if isinstance(key, asyncio.Task): + self._task_states[key] = value + else: + 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: + 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() # @@ -783,7 +835,7 @@ class TaskGroup(abc.TaskGroup): 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) @@ -840,16 +892,26 @@ class TaskGroup(abc.TaskGroup): 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) - task.add_done_callback(task_done) - # 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) + finally: + del _task_states[coro] + + _task_states[task] = task_state self.cancel_scope._tasks.add(task) self._tasks.add(task) + + 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( @@ -1718,8 +1780,8 @@ class ConnectedUNIXDatagramSocket(_RawSocketMixin, abc.ConnectedUNIXDatagramSock return -_read_events: RunVar[dict[Any, asyncio.Event]] = RunVar("read_events") -_write_events: RunVar[dict[Any, asyncio.Event]] = RunVar("write_events") +_read_events: RunVar[dict[int, asyncio.Event]] = RunVar("read_events") +_write_events: RunVar[dict[int, asyncio.Event]] = RunVar("write_events") # @@ -2082,7 +2144,9 @@ class AsyncIOTaskInfo(TaskInfo): 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: @@ -2090,12 +2154,11 @@ class AsyncIOTaskInfo(TaskInfo): # If the task isn't around anymore, it won't have a pending cancellation return False - if sys.version_info >= (3, 11): - if task.cancelling(): - return True + if task._must_cancel: # type: ignore[attr-defined] + return True elif ( - isinstance(task._fut_waiter, asyncio.Future) - and task._fut_waiter.cancelled() + isinstance(task._fut_waiter, asyncio.Future) # type: ignore[attr-defined] + and task._fut_waiter.cancelled() # type: ignore[attr-defined] ): return True @@ -2335,10 +2398,11 @@ class AsyncIOBackend(AsyncBackend): @classmethod def current_effective_deadline(cls) -> float: + if (task := current_task()) is None: + return math.inf + try: - cancel_scope = _task_states[ - current_task() # type: ignore[index] - ].cancel_scope + cancel_scope = _task_states[task].cancel_scope except KeyError: return math.inf @@ -2671,7 +2735,7 @@ class AsyncIOBackend(AsyncBackend): return await get_running_loop().getnameinfo(sockaddr, flags) @classmethod - async def wait_socket_readable(cls, sock: socket.socket) -> None: + async def wait_readable(cls, obj: FileDescriptorLike) -> None: await cls.checkpoint() try: read_events = _read_events.get() @@ -2679,26 +2743,34 @@ class AsyncIOBackend(AsyncBackend): read_events = {} _read_events.set(read_events) - if read_events.get(sock): - raise BusyResourceError("reading from") from None + if not isinstance(obj, int): + obj = obj.fileno() + + if read_events.get(obj): + raise BusyResourceError("reading from") loop = get_running_loop() - event = read_events[sock] = asyncio.Event() - loop.add_reader(sock, event.set) + event = asyncio.Event() + try: + loop.add_reader(obj, event.set) + except NotImplementedError: + from anyio._core._asyncio_selector_thread import get_selector + + selector = get_selector() + selector.add_reader(obj, event.set) + remove_reader = selector.remove_reader + else: + remove_reader = loop.remove_reader + + read_events[obj] = event try: await event.wait() finally: - if read_events.pop(sock, None) is not None: - loop.remove_reader(sock) - readable = True - else: - readable = False - - if not readable: - raise ClosedResourceError + remove_reader(obj) + del read_events[obj] @classmethod - async def wait_socket_writable(cls, sock: socket.socket) -> None: + async def wait_writable(cls, obj: FileDescriptorLike) -> None: await cls.checkpoint() try: write_events = _write_events.get() @@ -2706,23 +2778,31 @@ class AsyncIOBackend(AsyncBackend): write_events = {} _write_events.set(write_events) - if write_events.get(sock): - raise BusyResourceError("writing to") from None + if not isinstance(obj, int): + obj = obj.fileno() + + if write_events.get(obj): + raise BusyResourceError("writing to") loop = get_running_loop() - event = write_events[sock] = asyncio.Event() - loop.add_writer(sock.fileno(), event.set) + event = asyncio.Event() + try: + loop.add_writer(obj, event.set) + except NotImplementedError: + from anyio._core._asyncio_selector_thread import get_selector + + selector = get_selector() + selector.add_writer(obj, event.set) + remove_writer = selector.remove_writer + else: + remove_writer = loop.remove_writer + + write_events[obj] = event try: await event.wait() finally: - if write_events.pop(sock, None) is not None: - loop.remove_writer(sock) - writable = True - else: - writable = False - - if not writable: - raise ClosedResourceError + del write_events[obj] + remove_writer(obj) @classmethod def current_default_thread_limiter(cls) -> CapacityLimiter: diff --git a/contrib/python/anyio/anyio/_backends/_trio.py b/contrib/python/anyio/anyio/_backends/_trio.py index 24dcd744461..70a0a605781 100644 --- a/contrib/python/anyio/anyio/_backends/_trio.py +++ b/contrib/python/anyio/anyio/_backends/_trio.py @@ -28,6 +28,7 @@ from socket import AddressFamily, SocketKind from types import TracebackType from typing import ( IO, + TYPE_CHECKING, Any, Generic, NoReturn, @@ -80,6 +81,9 @@ from ..abc import IPSockAddrType, UDPPacketType, UNIXDatagramPacketType from ..abc._eventloop import AsyncBackend, StrOrBytesPath from ..streams.memory import MemoryObjectSendStream +if TYPE_CHECKING: + from _typeshed import HasFileno + if sys.version_info >= (3, 10): from typing import ParamSpec else: @@ -1260,18 +1264,18 @@ class TrioBackend(AsyncBackend): return await trio.socket.getnameinfo(sockaddr, flags) @classmethod - async def wait_socket_readable(cls, sock: socket.socket) -> None: + async def wait_readable(cls, obj: HasFileno | int) -> None: try: - await wait_readable(sock) + await wait_readable(obj) except trio.ClosedResourceError as exc: raise ClosedResourceError().with_traceback(exc.__traceback__) from None except trio.BusyResourceError: raise BusyResourceError("reading from") from None @classmethod - async def wait_socket_writable(cls, sock: socket.socket) -> None: + async def wait_writable(cls, obj: HasFileno | int) -> None: try: - await wait_writable(sock) + await wait_writable(obj) except trio.ClosedResourceError as exc: raise ClosedResourceError().with_traceback(exc.__traceback__) from None except trio.BusyResourceError: diff --git a/contrib/python/anyio/anyio/_core/_asyncio_selector_thread.py b/contrib/python/anyio/anyio/_core/_asyncio_selector_thread.py new file mode 100644 index 00000000000..d98c3040721 --- /dev/null +++ b/contrib/python/anyio/anyio/_core/_asyncio_selector_thread.py @@ -0,0 +1,150 @@ +from __future__ import annotations + +import asyncio +import socket +import threading +from collections.abc import Callable +from selectors import EVENT_READ, EVENT_WRITE, DefaultSelector +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from _typeshed import FileDescriptorLike + +_selector_lock = threading.Lock() +_selector: Selector | None = None + + +class Selector: + def __init__(self) -> None: + self._thread = threading.Thread(target=self.run, name="AnyIO socket selector") + self._selector = DefaultSelector() + self._send, self._receive = socket.socketpair() + self._send.setblocking(False) + self._receive.setblocking(False) + self._selector.register(self._receive, EVENT_READ) + self._closed = False + + def start(self) -> None: + self._thread.start() + threading._register_atexit(self._stop) # type: ignore[attr-defined] + + def _stop(self) -> None: + global _selector + self._closed = True + self._notify_self() + self._send.close() + self._thread.join() + self._selector.unregister(self._receive) + self._receive.close() + self._selector.close() + _selector = None + assert ( + not self._selector.get_map() + ), "selector still has registered file descriptors after shutdown" + + def _notify_self(self) -> None: + try: + self._send.send(b"\x00") + except BlockingIOError: + pass + + def add_reader(self, fd: FileDescriptorLike, callback: Callable[[], Any]) -> None: + loop = asyncio.get_running_loop() + try: + key = self._selector.get_key(fd) + except KeyError: + self._selector.register(fd, EVENT_READ, {EVENT_READ: (loop, callback)}) + else: + if EVENT_READ in key.data: + raise ValueError( + "this file descriptor is already registered for reading" + ) + + key.data[EVENT_READ] = loop, callback + self._selector.modify(fd, key.events | EVENT_READ, key.data) + + self._notify_self() + + def add_writer(self, fd: FileDescriptorLike, callback: Callable[[], Any]) -> None: + loop = asyncio.get_running_loop() + try: + key = self._selector.get_key(fd) + except KeyError: + self._selector.register(fd, EVENT_WRITE, {EVENT_WRITE: (loop, callback)}) + else: + if EVENT_WRITE in key.data: + raise ValueError( + "this file descriptor is already registered for writing" + ) + + key.data[EVENT_WRITE] = loop, callback + self._selector.modify(fd, key.events | EVENT_WRITE, key.data) + + self._notify_self() + + def remove_reader(self, fd: FileDescriptorLike) -> bool: + try: + key = self._selector.get_key(fd) + except KeyError: + return False + + if new_events := key.events ^ EVENT_READ: + del key.data[EVENT_READ] + self._selector.modify(fd, new_events, key.data) + else: + self._selector.unregister(fd) + + return True + + def remove_writer(self, fd: FileDescriptorLike) -> bool: + try: + key = self._selector.get_key(fd) + except KeyError: + return False + + if new_events := key.events ^ EVENT_WRITE: + del key.data[EVENT_WRITE] + self._selector.modify(fd, new_events, key.data) + else: + self._selector.unregister(fd) + + return True + + def run(self) -> None: + while not self._closed: + for key, events in self._selector.select(): + if key.fileobj is self._receive: + try: + while self._receive.recv(4096): + pass + except BlockingIOError: + pass + + continue + + if events & EVENT_READ: + loop, callback = key.data[EVENT_READ] + self.remove_reader(key.fd) + try: + loop.call_soon_threadsafe(callback) + except RuntimeError: + pass # the loop was already closed + + if events & EVENT_WRITE: + loop, callback = key.data[EVENT_WRITE] + self.remove_writer(key.fd) + try: + loop.call_soon_threadsafe(callback) + except RuntimeError: + pass # the loop was already closed + + +def get_selector() -> Selector: + global _selector + + with _selector_lock: + if _selector is None: + _selector = Selector() + _selector.start() + + return _selector diff --git a/contrib/python/anyio/anyio/_core/_exceptions.py b/contrib/python/anyio/anyio/_core/_exceptions.py index 6e3f8ccc675..97ea3130414 100644 --- a/contrib/python/anyio/anyio/_core/_exceptions.py +++ b/contrib/python/anyio/anyio/_core/_exceptions.py @@ -16,7 +16,7 @@ class BrokenResourceError(Exception): class BrokenWorkerProcess(Exception): """ - Raised by :func:`run_sync_in_process` if the worker process terminates abruptly or + Raised by :meth:`~anyio.to_process.run_sync` if the worker process terminates abruptly or otherwise misbehaves. """ diff --git a/contrib/python/anyio/anyio/_core/_fileio.py b/contrib/python/anyio/anyio/_core/_fileio.py index 53d3288c295..ef2930e480e 100644 --- a/contrib/python/anyio/anyio/_core/_fileio.py +++ b/contrib/python/anyio/anyio/_core/_fileio.py @@ -92,10 +92,10 @@ class AsyncFile(AsyncResource, Generic[AnyStr]): async def readlines(self) -> list[AnyStr]: return await to_thread.run_sync(self._fp.readlines) - async def readinto(self: AsyncFile[bytes], b: WriteableBuffer) -> bytes: + async def readinto(self: AsyncFile[bytes], b: WriteableBuffer) -> int: return await to_thread.run_sync(self._fp.readinto, b) - async def readinto1(self: AsyncFile[bytes], b: WriteableBuffer) -> bytes: + async def readinto1(self: AsyncFile[bytes], b: WriteableBuffer) -> int: return await to_thread.run_sync(self._fp.readinto1, b) @overload diff --git a/contrib/python/anyio/anyio/_core/_sockets.py b/contrib/python/anyio/anyio/_core/_sockets.py index 6070c647fd9..a822d060d72 100644 --- a/contrib/python/anyio/anyio/_core/_sockets.py +++ b/contrib/python/anyio/anyio/_core/_sockets.py @@ -10,7 +10,7 @@ from collections.abc import Awaitable from ipaddress import IPv6Address, ip_address from os import PathLike, chmod from socket import AddressFamily, SocketKind -from typing import Any, Literal, cast, overload +from typing import TYPE_CHECKING, Any, Literal, cast, overload from .. import to_thread from ..abc import ( @@ -31,9 +31,19 @@ from ._resources import aclose_forcefully from ._synchronization import Event from ._tasks import create_task_group, move_on_after +if TYPE_CHECKING: + from _typeshed import FileDescriptorLike +else: + FileDescriptorLike = object + if sys.version_info < (3, 11): from exceptiongroup import ExceptionGroup +if sys.version_info < (3, 13): + from typing_extensions import deprecated +else: + from warnings import deprecated + IPPROTO_IPV6 = getattr(socket, "IPPROTO_IPV6", 41) # https://bugs.python.org/issue29515 AnyIPAddressFamily = Literal[ @@ -186,6 +196,14 @@ async def connect_tcp( try: addr_obj = ip_address(remote_host) except ValueError: + addr_obj = None + + if addr_obj is not None: + if isinstance(addr_obj, IPv6Address): + target_addrs = [(socket.AF_INET6, addr_obj.compressed)] + else: + target_addrs = [(socket.AF_INET, addr_obj.compressed)] + else: # getaddrinfo() will raise an exception if name resolution fails gai_res = await getaddrinfo( target_host, remote_port, family=family, type=socket.SOCK_STREAM @@ -194,7 +212,7 @@ async def connect_tcp( # Organize the list so that the first address is an IPv6 address (if available) # and the second one is an IPv4 addresses. The rest can be in whatever order. v6_found = v4_found = False - target_addrs: list[tuple[socket.AddressFamily, str]] = [] + target_addrs = [] for af, *rest, sa in gai_res: if af == socket.AF_INET6 and not v6_found: v6_found = True @@ -204,11 +222,6 @@ async def connect_tcp( target_addrs.insert(1, (af, sa[0])) else: target_addrs.append((af, sa[0])) - else: - if isinstance(addr_obj, IPv6Address): - target_addrs = [(socket.AF_INET6, addr_obj.compressed)] - else: - target_addrs = [(socket.AF_INET, addr_obj.compressed)] oserrors: list[OSError] = [] async with create_task_group() as tg: @@ -588,12 +601,13 @@ def getnameinfo(sockaddr: IPSockAddrType, flags: int = 0) -> Awaitable[tuple[str return get_async_backend().getnameinfo(sockaddr, flags) +@deprecated("This function is deprecated; use `wait_readable` instead") def wait_socket_readable(sock: socket.socket) -> Awaitable[None]: """ - Wait until the given socket has data to be read. + .. deprecated:: 4.7.0 + Use :func:`wait_readable` instead. - This does **NOT** work on Windows when using the asyncio backend with a proactor - event loop (default on py3.8+). + Wait until the given socket has data to be read. .. warning:: Only use this on raw sockets that have not been wrapped by any higher level constructs like socket streams! @@ -605,11 +619,15 @@ def wait_socket_readable(sock: socket.socket) -> Awaitable[None]: to become readable """ - return get_async_backend().wait_socket_readable(sock) + return get_async_backend().wait_readable(sock.fileno()) +@deprecated("This function is deprecated; use `wait_writable` instead") def wait_socket_writable(sock: socket.socket) -> Awaitable[None]: """ + .. deprecated:: 4.7.0 + Use :func:`wait_writable` instead. + Wait until the given socket can be written to. This does **NOT** work on Windows when using the asyncio backend with a proactor @@ -625,7 +643,58 @@ def wait_socket_writable(sock: socket.socket) -> Awaitable[None]: to become writable """ - return get_async_backend().wait_socket_writable(sock) + return get_async_backend().wait_writable(sock.fileno()) + + +def wait_readable(obj: FileDescriptorLike) -> Awaitable[None]: + """ + Wait until the given object has data to be read. + + On Unix systems, ``obj`` must either be an integer file descriptor, or else an + object with a ``.fileno()`` method which returns an integer file descriptor. Any + kind of file descriptor can be passed, though the exact semantics will depend on + your kernel. For example, this probably won't do anything useful for on-disk files. + + On Windows systems, ``obj`` must either be an integer ``SOCKET`` handle, or else an + object with a ``.fileno()`` method which returns an integer ``SOCKET`` handle. File + descriptors aren't supported, and neither are handles that refer to anything besides + a ``SOCKET``. + + On backends where this functionality is not natively provided (asyncio + ``ProactorEventLoop`` on Windows), it is provided using a separate selector thread + which is set to shut down when the interpreter shuts down. + + .. warning:: Don't use this on raw sockets that have been wrapped by any higher + level constructs like socket streams! + + :param obj: an object with a ``.fileno()`` method or an integer handle + :raises ~anyio.ClosedResourceError: if the object was closed while waiting for the + object to become readable + :raises ~anyio.BusyResourceError: if another task is already waiting for the object + to become readable + + """ + return get_async_backend().wait_readable(obj) + + +def wait_writable(obj: FileDescriptorLike) -> Awaitable[None]: + """ + Wait until the given object can be written to. + + :param obj: an object with a ``.fileno()`` method or an integer handle + :raises ~anyio.ClosedResourceError: if the object was closed while waiting for the + object to become writable + :raises ~anyio.BusyResourceError: if another task is already waiting for the object + to become writable + + .. seealso:: See the documentation of :func:`wait_readable` for the definition of + ``obj`` and notes on backend compatibility. + + .. warning:: Don't use this on raw sockets that have been wrapped by any higher + level constructs like socket streams! + + """ + return get_async_backend().wait_writable(obj) # diff --git a/contrib/python/anyio/anyio/_core/_synchronization.py b/contrib/python/anyio/anyio/_core/_synchronization.py index 023ab73370d..7878ba66682 100644 --- a/contrib/python/anyio/anyio/_core/_synchronization.py +++ b/contrib/python/anyio/anyio/_core/_synchronization.py @@ -109,6 +109,7 @@ class Event: class EventAdapter(Event): _internal_event: Event | None = None + _is_set: bool = False def __new__(cls) -> EventAdapter: return object.__new__(cls) @@ -117,14 +118,22 @@ class EventAdapter(Event): def _event(self) -> Event: if self._internal_event is None: self._internal_event = get_async_backend().create_event() + if self._is_set: + self._internal_event.set() return self._internal_event def set(self) -> None: - self._event.set() + if self._internal_event is None: + self._is_set = True + else: + self._event.set() def is_set(self) -> bool: - return self._internal_event is not None and self._internal_event.is_set() + if self._internal_event is None: + return self._is_set + + return self._internal_event.is_set() async def wait(self) -> None: await self._event.wait() diff --git a/contrib/python/anyio/anyio/abc/__init__.py b/contrib/python/anyio/anyio/abc/__init__.py index 1ca0fcf746e..3d3b61cc9a0 100644 --- a/contrib/python/anyio/anyio/abc/__init__.py +++ b/contrib/python/anyio/anyio/abc/__init__.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Any - from ._eventloop import AsyncBackend as AsyncBackend from ._resources import AsyncResource as AsyncResource from ._sockets import ConnectedUDPSocket as ConnectedUDPSocket @@ -50,8 +48,8 @@ from .._core._tasks import CancelScope as CancelScope from ..from_thread import BlockingPortal as BlockingPortal # Re-export imports so they look like they live directly in this package -key: str -value: Any -for key, value in list(locals().items()): - if getattr(value, "__module__", "").startswith("anyio.abc."): - value.__module__ = __name__ +for __value in list(locals().values()): + if getattr(__value, "__module__", "").startswith("anyio.abc."): + __value.__module__ = __name__ + +del __value diff --git a/contrib/python/anyio/anyio/abc/_eventloop.py b/contrib/python/anyio/anyio/abc/_eventloop.py index 93d0e9d25b4..2bfdf286357 100644 --- a/contrib/python/anyio/anyio/abc/_eventloop.py +++ b/contrib/python/anyio/anyio/abc/_eventloop.py @@ -28,6 +28,8 @@ else: from typing_extensions import TypeAlias if TYPE_CHECKING: + from _typeshed import HasFileno + from .._core._synchronization import CapacityLimiter, Event, Lock, Semaphore from .._core._tasks import CancelScope from .._core._testing import TaskInfo @@ -333,12 +335,12 @@ class AsyncBackend(metaclass=ABCMeta): @classmethod @abstractmethod - async def wait_socket_readable(cls, sock: socket) -> None: + async def wait_readable(cls, obj: HasFileno | int) -> None: pass @classmethod @abstractmethod - async def wait_socket_writable(cls, sock: socket) -> None: + async def wait_writable(cls, obj: HasFileno | int) -> None: pass @classmethod diff --git a/contrib/python/anyio/anyio/abc/_tasks.py b/contrib/python/anyio/anyio/abc/_tasks.py index 88aecf38334..f6e5c40c7ff 100644 --- a/contrib/python/anyio/anyio/abc/_tasks.py +++ b/contrib/python/anyio/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 diff --git a/contrib/python/anyio/ya.make b/contrib/python/anyio/ya.make index aadbb5b2978..956fc4c8415 100644 --- a/contrib/python/anyio/ya.make +++ b/contrib/python/anyio/ya.make @@ -2,13 +2,14 @@ PY3_LIBRARY() -VERSION(4.6.2.post1) +VERSION(4.7.0) LICENSE(MIT) PEERDIR( contrib/python/idna contrib/python/sniffio + contrib/python/typing-extensions ) NO_LINT() @@ -25,6 +26,7 @@ PY_SRCS( anyio/_backends/_asyncio.py anyio/_backends/_trio.py anyio/_core/__init__.py + anyio/_core/_asyncio_selector_thread.py anyio/_core/_eventloop.py anyio/_core/_exceptions.py anyio/_core/_fileio.py diff --git a/contrib/python/fonttools/.dist-info/METADATA b/contrib/python/fonttools/.dist-info/METADATA index 9460888006d..5a177ba30c9 100644 --- a/contrib/python/fonttools/.dist-info/METADATA +++ b/contrib/python/fonttools/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: fonttools -Version: 4.55.1 +Version: 4.55.2 Summary: Tools to manipulate font files Home-page: http://github.com/fonttools/fonttools Author: Just van Rossum @@ -377,6 +377,13 @@ Have fun! Changelog ~~~~~~~~~ +4.55.2 (released 2024-12-05) +---------------------------- + +- [Docs] update Sphinx config (#3712) +- [designspaceLib] Allow axisOrdering to be set to zero (#3715) +- [feaLib] Don’t modify variable anchors in place (#3717) + 4.55.1 (released 2024-12-02) ---------------------------- diff --git a/contrib/python/fonttools/fontTools/__init__.py b/contrib/python/fonttools/fontTools/__init__.py index 6aa5f3ad593..2fe3602d325 100644 --- a/contrib/python/fonttools/fontTools/__init__.py +++ b/contrib/python/fonttools/fontTools/__init__.py @@ -3,6 +3,6 @@ from fontTools.misc.loggingTools import configLogger log = logging.getLogger(__name__) -version = __version__ = "4.55.1" +version = __version__ = "4.55.2" __all__ = ["version", "log", "configLogger"] diff --git a/contrib/python/fonttools/fontTools/designspaceLib/__init__.py b/contrib/python/fonttools/fontTools/designspaceLib/__init__.py index 0a1e782f57c..661f3405da1 100644 --- a/contrib/python/fonttools/fontTools/designspaceLib/__init__.py +++ b/contrib/python/fonttools/fontTools/designspaceLib/__init__.py @@ -1596,7 +1596,7 @@ class BaseDocWriter(object): mapElement.attrib["input"] = self.intOrFloat(inputValue) mapElement.attrib["output"] = self.intOrFloat(outputValue) axisElement.append(mapElement) - if axisObject.axisOrdering or axisObject.axisLabels: + if axisObject.axisOrdering is not None or axisObject.axisLabels: labelsElement = ET.Element("labels") if axisObject.axisOrdering is not None: labelsElement.attrib["ordering"] = str(axisObject.axisOrdering) diff --git a/contrib/python/fonttools/fontTools/feaLib/builder.py b/contrib/python/fonttools/fontTools/feaLib/builder.py index bda855e1e94..81aa8c2e263 100644 --- a/contrib/python/fonttools/fontTools/feaLib/builder.py +++ b/contrib/python/fonttools/fontTools/feaLib/builder.py @@ -1658,38 +1658,31 @@ class Builder(object): return default, device + def makeAnchorPos(self, varscalar, deviceTable, location): + device = None + if not isinstance(varscalar, VariableScalar): + if deviceTable is not None: + device = otl.buildDevice(dict(deviceTable)) + return varscalar, device + default, device = self.makeVariablePos(location, varscalar) + if device is not None and deviceTable is not None: + raise FeatureLibError( + "Can't define a device coordinate and variable scalar", location + ) + return default, device + def makeOpenTypeAnchor(self, location, anchor): """ast.Anchor --> otTables.Anchor""" if anchor is None: return None - variable = False deviceX, deviceY = None, None if anchor.xDeviceTable is not None: deviceX = otl.buildDevice(dict(anchor.xDeviceTable)) if anchor.yDeviceTable is not None: deviceY = otl.buildDevice(dict(anchor.yDeviceTable)) - for dim in ("x", "y"): - varscalar = getattr(anchor, dim) - if not isinstance(varscalar, VariableScalar): - continue - if getattr(anchor, dim + "DeviceTable") is not None: - raise FeatureLibError( - "Can't define a device coordinate and variable scalar", location - ) - default, device = self.makeVariablePos(location, varscalar) - setattr(anchor, dim, default) - if device is not None: - if dim == "x": - deviceX = device - else: - deviceY = device - variable = True - - otlanchor = otl.buildAnchor( - anchor.x, anchor.y, anchor.contourpoint, deviceX, deviceY - ) - if variable: - otlanchor.Format = 3 + x, deviceX = self.makeAnchorPos(anchor.x, anchor.xDeviceTable, location) + y, deviceY = self.makeAnchorPos(anchor.y, anchor.yDeviceTable, location) + otlanchor = otl.buildAnchor(x, y, anchor.contourpoint, deviceX, deviceY) return otlanchor _VALUEREC_ATTRS = { diff --git a/contrib/python/fonttools/ya.make b/contrib/python/fonttools/ya.make index ce8febfcdd8..d01a0e414f3 100644 --- a/contrib/python/fonttools/ya.make +++ b/contrib/python/fonttools/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(4.55.1) +VERSION(4.55.2) LICENSE(MIT) |
