From 0f5fc6cbafa3e7c84f258d1865996d97ae67a318 Mon Sep 17 00:00:00 2001 From: EXPLOSION Date: Thu, 13 Jun 2024 18:16:54 -0400 Subject: [PATCH] Fixes for 3.13 (#3005) * Try out naive KI protection fix * Update generated files * Account for `._local` in full name * Add 3.13 to CI matrix * Avoid cryptography * Avoid an evil operation, I guess * un-inline ki protection decorator new f_locals method doesn't create a cycle in this situation * Advertise 3.12 + maybe cryptography works? * Switch back to avoiding cryptography * Try cryptography one more time * Add symbols * Try one other possibility for symbols * More symbol trial and error * Try to pick up patterns in what allows what * Test hypothesis * Finish off failures * Remove trailing whitespace --------- Co-authored-by: richardsheridan --- .github/workflows/ci.yml | 2 +- pyproject.toml | 1 + src/trio/_core/_generated_instrumentation.py | 5 +++-- src/trio/_core/_generated_io_epoll.py | 8 ++++---- src/trio/_core/_generated_io_kqueue.py | 14 +++++++------- src/trio/_core/_generated_io_windows.py | 20 ++++++++++---------- src/trio/_core/_generated_run.py | 17 +++++++++-------- src/trio/_core/_ki.py | 2 +- src/trio/_core/_run.py | 11 ++--------- src/trio/_path.py | 2 ++ src/trio/_tests/test_exports.py | 14 ++++++++++++++ src/trio/_tests/test_path.py | 2 +- src/trio/_tools/gen_exports.py | 4 +++- src/trio/socket.py | 2 ++ test-requirements.txt | 2 +- 15 files changed, 61 insertions(+), 45 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa7e67a9e2..c5c04c1a3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['pypy-3.9', 'pypy-3.10', '3.8', '3.9', '3.10', '3.11', '3.12'] + python: ['pypy-3.9', 'pypy-3.10', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] check_formatting: ['0'] no_test_requirements: ['0'] extra_name: [''] diff --git a/pyproject.toml b/pyproject.toml index 2ad8e4e05d..0e26fea83a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: System :: Networking", "Typing :: Typed", ] diff --git a/src/trio/_core/_generated_instrumentation.py b/src/trio/_core/_generated_instrumentation.py index e9c7250f6e..568b76dffa 100644 --- a/src/trio/_core/_generated_instrumentation.py +++ b/src/trio/_core/_generated_instrumentation.py @@ -3,6 +3,7 @@ # ************************************************************* from __future__ import annotations +import sys from typing import TYPE_CHECKING from ._ki import LOCALS_KEY_KI_PROTECTION_ENABLED @@ -23,7 +24,7 @@ def add_instrument(instrument: Instrument) -> None: If ``instrument`` is already active, does nothing. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return GLOBAL_RUN_CONTEXT.runner.instruments.add_instrument(instrument) except AttributeError: @@ -43,7 +44,7 @@ def remove_instrument(instrument: Instrument) -> None: deactivated. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return GLOBAL_RUN_CONTEXT.runner.instruments.remove_instrument(instrument) except AttributeError: diff --git a/src/trio/_core/_generated_io_epoll.py b/src/trio/_core/_generated_io_epoll.py index aa2c6eda68..9f9ad59725 100644 --- a/src/trio/_core/_generated_io_epoll.py +++ b/src/trio/_core/_generated_io_epoll.py @@ -3,6 +3,7 @@ # ************************************************************* from __future__ import annotations +import sys from typing import TYPE_CHECKING from ._ki import LOCALS_KEY_KI_PROTECTION_ENABLED @@ -10,7 +11,6 @@ if TYPE_CHECKING: from .._file_io import _HasFileNo -import sys assert not TYPE_CHECKING or sys.platform == "linux" @@ -40,7 +40,7 @@ async def wait_readable(fd: int | _HasFileNo) -> None: if another task calls :func:`notify_closing` while this function is still working. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return await GLOBAL_RUN_CONTEXT.runner.io_manager.wait_readable(fd) except AttributeError: @@ -59,7 +59,7 @@ async def wait_writable(fd: int | _HasFileNo) -> None: if another task calls :func:`notify_closing` while this function is still working. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return await GLOBAL_RUN_CONTEXT.runner.io_manager.wait_writable(fd) except AttributeError: @@ -91,7 +91,7 @@ def notify_closing(fd: int | _HasFileNo) -> None: step, so other tasks won't be able to tell what order they happened in anyway. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return GLOBAL_RUN_CONTEXT.runner.io_manager.notify_closing(fd) except AttributeError: diff --git a/src/trio/_core/_generated_io_kqueue.py b/src/trio/_core/_generated_io_kqueue.py index f1c6634147..e150fc21f9 100644 --- a/src/trio/_core/_generated_io_kqueue.py +++ b/src/trio/_core/_generated_io_kqueue.py @@ -3,6 +3,7 @@ # ************************************************************* from __future__ import annotations +import sys from typing import TYPE_CHECKING, Callable, ContextManager from ._ki import LOCALS_KEY_KI_PROTECTION_ENABLED @@ -14,7 +15,6 @@ from .. import _core from .._file_io import _HasFileNo from ._traps import Abort, RaiseCancelT -import sys assert not TYPE_CHECKING or sys.platform == "darwin" @@ -34,7 +34,7 @@ def current_kqueue() -> select.kqueue: anything real. See `#26 `__. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return GLOBAL_RUN_CONTEXT.runner.io_manager.current_kqueue() except AttributeError: @@ -48,7 +48,7 @@ def monitor_kevent( anything real. See `#26 `__. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return GLOBAL_RUN_CONTEXT.runner.io_manager.monitor_kevent(ident, filter) except AttributeError: @@ -62,7 +62,7 @@ async def wait_kevent( anything real. See `#26 `__. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return await GLOBAL_RUN_CONTEXT.runner.io_manager.wait_kevent( ident, filter, abort_func @@ -93,7 +93,7 @@ async def wait_readable(fd: int | _HasFileNo) -> None: if another task calls :func:`notify_closing` while this function is still working. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return await GLOBAL_RUN_CONTEXT.runner.io_manager.wait_readable(fd) except AttributeError: @@ -112,7 +112,7 @@ async def wait_writable(fd: int | _HasFileNo) -> None: if another task calls :func:`notify_closing` while this function is still working. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return await GLOBAL_RUN_CONTEXT.runner.io_manager.wait_writable(fd) except AttributeError: @@ -144,7 +144,7 @@ def notify_closing(fd: int | _HasFileNo) -> None: step, so other tasks won't be able to tell what order they happened in anyway. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return GLOBAL_RUN_CONTEXT.runner.io_manager.notify_closing(fd) except AttributeError: diff --git a/src/trio/_core/_generated_io_windows.py b/src/trio/_core/_generated_io_windows.py index b4ebe85303..72264f599f 100644 --- a/src/trio/_core/_generated_io_windows.py +++ b/src/trio/_core/_generated_io_windows.py @@ -3,6 +3,7 @@ # ************************************************************* from __future__ import annotations +import sys from typing import TYPE_CHECKING, ContextManager from ._ki import LOCALS_KEY_KI_PROTECTION_ENABLED @@ -14,7 +15,6 @@ from .._file_io import _HasFileNo from ._unbounded_queue import UnboundedQueue from ._windows_cffi import CData, Handle -import sys assert not TYPE_CHECKING or sys.platform == "win32" @@ -54,7 +54,7 @@ async def wait_readable(sock: _HasFileNo | int) -> None: if another task calls :func:`notify_closing` while this function is still working. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return await GLOBAL_RUN_CONTEXT.runner.io_manager.wait_readable(sock) except AttributeError: @@ -73,7 +73,7 @@ async def wait_writable(sock: _HasFileNo | int) -> None: if another task calls :func:`notify_closing` while this function is still working. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return await GLOBAL_RUN_CONTEXT.runner.io_manager.wait_writable(sock) except AttributeError: @@ -105,7 +105,7 @@ def notify_closing(handle: Handle | int | _HasFileNo) -> None: step, so other tasks won't be able to tell what order they happened in anyway. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return GLOBAL_RUN_CONTEXT.runner.io_manager.notify_closing(handle) except AttributeError: @@ -118,7 +118,7 @@ def register_with_iocp(handle: int | CData) -> None: `__ and `#52 `__. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return GLOBAL_RUN_CONTEXT.runner.io_manager.register_with_iocp(handle) except AttributeError: @@ -131,7 +131,7 @@ async def wait_overlapped(handle_: int | CData, lpOverlapped: CData | int) -> ob `__ and `#52 `__. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return await GLOBAL_RUN_CONTEXT.runner.io_manager.wait_overlapped( handle_, lpOverlapped @@ -148,7 +148,7 @@ async def write_overlapped( `__ and `#52 `__. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return await GLOBAL_RUN_CONTEXT.runner.io_manager.write_overlapped( handle, data, file_offset @@ -165,7 +165,7 @@ async def readinto_overlapped( `__ and `#52 `__. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return await GLOBAL_RUN_CONTEXT.runner.io_manager.readinto_overlapped( handle, buffer, file_offset @@ -180,7 +180,7 @@ def current_iocp() -> int: `__ and `#52 `__. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return GLOBAL_RUN_CONTEXT.runner.io_manager.current_iocp() except AttributeError: @@ -193,7 +193,7 @@ def monitor_completion_key() -> ContextManager[tuple[int, UnboundedQueue[object] `__ and `#52 `__. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return GLOBAL_RUN_CONTEXT.runner.io_manager.monitor_completion_key() except AttributeError: diff --git a/src/trio/_core/_generated_run.py b/src/trio/_core/_generated_run.py index 47febd3ca1..ac3e0f39d6 100644 --- a/src/trio/_core/_generated_run.py +++ b/src/trio/_core/_generated_run.py @@ -3,6 +3,7 @@ # ************************************************************* from __future__ import annotations +import sys from typing import TYPE_CHECKING, Any from ._ki import LOCALS_KEY_KI_PROTECTION_ENABLED @@ -55,7 +56,7 @@ def current_statistics() -> RunStatistics: other attributes vary between backends. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return GLOBAL_RUN_CONTEXT.runner.current_statistics() except AttributeError: @@ -72,7 +73,7 @@ def current_time() -> float: RuntimeError: if not inside a call to :func:`trio.run`. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return GLOBAL_RUN_CONTEXT.runner.current_time() except AttributeError: @@ -81,7 +82,7 @@ def current_time() -> float: def current_clock() -> Clock: """Returns the current :class:`~trio.abc.Clock`.""" - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return GLOBAL_RUN_CONTEXT.runner.current_clock() except AttributeError: @@ -94,7 +95,7 @@ def current_root_task() -> Task | None: This is the task that is the ultimate parent of all other tasks. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return GLOBAL_RUN_CONTEXT.runner.current_root_task() except AttributeError: @@ -119,7 +120,7 @@ def reschedule(task: Task, next_send: Outcome[Any] = _NO_SEND) -> None: raise) from :func:`wait_task_rescheduled`. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return GLOBAL_RUN_CONTEXT.runner.reschedule(task, next_send) except AttributeError: @@ -183,7 +184,7 @@ def spawn_system_task( Task: the newly spawned task """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return GLOBAL_RUN_CONTEXT.runner.spawn_system_task( async_fn, *args, name=name, context=context @@ -197,7 +198,7 @@ def current_trio_token() -> TrioToken: :func:`trio.run`. """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return GLOBAL_RUN_CONTEXT.runner.current_trio_token() except AttributeError: @@ -262,7 +263,7 @@ async def test_lock_fairness(): print("FAIL") """ - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return await GLOBAL_RUN_CONTEXT.runner.wait_all_tasks_blocked(cushion) except AttributeError: diff --git a/src/trio/_core/_ki.py b/src/trio/_core/_ki.py index c32f94e383..7d07caada2 100644 --- a/src/trio/_core/_ki.py +++ b/src/trio/_core/_ki.py @@ -178,7 +178,7 @@ def wrapper(*args: ArgsT.args, **kwargs: ArgsT.kwargs) -> RetT: # type: ignore[ @wraps(fn) def wrapper(*args: ArgsT.args, **kwargs: ArgsT.kwargs) -> RetT: - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = enabled + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = enabled return fn(*args, **kwargs) return wrapper diff --git a/src/trio/_core/_run.py b/src/trio/_core/_run.py index 89759cc2c2..df01519589 100644 --- a/src/trio/_core/_run.py +++ b/src/trio/_core/_run.py @@ -623,6 +623,7 @@ def _close(self, exc: BaseException | None) -> BaseException | None: self._cancel_status = None return exc + @enable_ki_protection def __exit__( self, etype: type[BaseException] | None, @@ -633,10 +634,6 @@ def __exit__( # so __exit__() must be just _close() plus this logic for adapting # the exception-filtering result to the context manager API. - # This inlines the enable_ki_protection decorator so we can fix - # f_locals *locally* below to avoid reference cycles - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True - # Tracebacks show the 'raise' line below out of context, so let's give # this variable a name that makes sense out of context. remaining_error_after_cancel_scope = self._close(exc) @@ -658,10 +655,6 @@ def __exit__( # see test_cancel_scope_exit_doesnt_create_cyclic_garbage # Note: still relevant del remaining_error_after_cancel_scope, value, _, exc - # deep magic to remove refs via f_locals - locals() - # TODO: check if PEP558 changes the need for this call - # https://github.com/python/cpython/pull/3640 def __repr__(self) -> str: if self._cancel_status is not None: @@ -2476,7 +2469,7 @@ def unrolled_run( args: tuple[Unpack[PosArgT]], host_uses_signal_set_wakeup_fd: bool = False, ) -> Generator[float, EventResult, None]: - locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True + sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True __tracebackhide__ = True try: diff --git a/src/trio/_path.py b/src/trio/_path.py index 907b0203f6..b9b5749c35 100644 --- a/src/trio/_path.py +++ b/src/trio/_path.py @@ -242,6 +242,8 @@ def __repr__(self) -> str: write_text = _wrap_method(pathlib.Path.write_text) if sys.version_info < (3, 12): link_to = _wrap_method(pathlib.Path.link_to) + if sys.version_info >= (3, 13): + full_match = _wrap_method(pathlib.Path.full_match) @final diff --git a/src/trio/_tests/test_exports.py b/src/trio/_tests/test_exports.py index 794f902d3e..26e9f6d516 100644 --- a/src/trio/_tests/test_exports.py +++ b/src/trio/_tests/test_exports.py @@ -346,6 +346,11 @@ def lookup_symbol(symbol: str) -> dict[str, str]: "__deepcopy__", } + if type(class_) == type: + # C extension classes don't have these dunders, but Python classes do + ignore_names.add("__firstlineno__") + ignore_names.add("__static_attributes__") + # pypy seems to have some additional dunders that differ if sys.implementation.name == "pypy": ignore_names |= { @@ -493,6 +498,15 @@ def lookup_symbol(symbol: str) -> dict[str, str]: if tool == "jedi" and sys.platform == "win32": extra -= {"owner", "is_mount", "group"} + # not sure why jedi in particular ignores this (static?) method in 3.13 + # (especially given the method is from 3.12....) + if ( + tool == "jedi" + and sys.version_info >= (3, 13) + and class_ in (trio.Path, trio.WindowsPath, trio.PosixPath) + ): + missing.remove("with_segments") + if missing or extra: # pragma: no cover errors[f"{module_name}.{class_name}"] = { "missing": missing, diff --git a/src/trio/_tests/test_path.py b/src/trio/_tests/test_path.py index db7aebf2df..af29a0604b 100644 --- a/src/trio/_tests/test_path.py +++ b/src/trio/_tests/test_path.py @@ -122,7 +122,7 @@ async def test_async_method_signature(path: trio.Path) -> None: assert path.resolve.__qualname__ == "Path.resolve" assert path.resolve.__doc__ is not None - assert "pathlib.Path.resolve" in path.resolve.__doc__ + assert path.resolve.__qualname__ in path.resolve.__doc__ @pytest.mark.parametrize("method_name", ["is_dir", "is_file"]) diff --git a/src/trio/_tools/gen_exports.py b/src/trio/_tools/gen_exports.py index 9b4558b57d..8cdefe2034 100755 --- a/src/trio/_tools/gen_exports.py +++ b/src/trio/_tools/gen_exports.py @@ -32,11 +32,13 @@ # ************************************************************* from __future__ import annotations +import sys + from ._ki import LOCALS_KEY_KI_PROTECTION_ENABLED from ._run import GLOBAL_RUN_CONTEXT """ -TEMPLATE = """locals()[LOCALS_KEY_KI_PROTECTION_ENABLED] = True +TEMPLATE = """sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return{}GLOBAL_RUN_CONTEXT.{}.{} except AttributeError: diff --git a/src/trio/socket.py b/src/trio/socket.py index 6319716143..900b341ec0 100644 --- a/src/trio/socket.py +++ b/src/trio/socket.py @@ -395,6 +395,7 @@ NETLINK_USERSOCK as NETLINK_USERSOCK, NETLINK_XFRM as NETLINK_XFRM, NI_DGRAM as NI_DGRAM, + NI_IDN as NI_IDN, NI_MAXHOST as NI_MAXHOST, NI_MAXSERV as NI_MAXSERV, NI_NAMEREQD as NI_NAMEREQD, @@ -443,6 +444,7 @@ SIOCGIFNAME as SIOCGIFNAME, SO_ACCEPTCONN as SO_ACCEPTCONN, SO_BINDTODEVICE as SO_BINDTODEVICE, + SO_BINDTOIFINDEX as SO_BINDTOIFINDEX, SO_BROADCAST as SO_BROADCAST, SO_DEBUG as SO_DEBUG, SO_DOMAIN as SO_DOMAIN, diff --git a/test-requirements.txt b/test-requirements.txt index 234e0b594c..2f29a427c0 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -24,7 +24,7 @@ build==1.2.1 # via pip-tools certifi==2024.2.2 # via requests -cffi==1.16.0 +cffi==1.17.0rc1 # via cryptography charset-normalizer==3.3.2 # via requests