Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions newsfragments/160.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix ``import trio_asyncio`` on Python 3.14+, where ``asyncio`` renamed
``BaseDefaultEventLoopPolicy`` and removed ``AbstractChildWatcher``.
50 changes: 50 additions & 0 deletions tests/test_py314_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Import / policy compatibility across asyncio API changes (esp. Python 3.14)."""

import asyncio
import sys

import pytest

import trio_asyncio
from trio_asyncio._loop import (
TrioChildWatcher,
TrioPolicy,
_BasePolicy,
_ChildWatcherBase,
_PlatformDefaultPolicy,
_trio_policy,
)


def test_package_imports():
assert trio_asyncio.__version__


def test_trio_policy_types():
assert isinstance(_trio_policy, TrioPolicy)
assert issubclass(TrioPolicy, _BasePolicy)
assert issubclass(TrioPolicy, _PlatformDefaultPolicy)
# Outside Trio context, get_event_loop_policy() returns the pre-import
# policy by design; the installed singleton is still TrioPolicy.
assert isinstance(_trio_policy, _BasePolicy)


def test_trio_child_watcher_constructible():
watcher = TrioChildWatcher()
assert isinstance(watcher, _ChildWatcherBase)
watcher.close()


@pytest.mark.skipif(sys.version_info < (3, 14), reason="3.14+ asyncio renames")
def test_uses_underscored_asyncio_names_on_3_14():
# BaseDefaultEventLoopPolicy was renamed; AbstractChildWatcher removed.
assert not hasattr(asyncio.events, "BaseDefaultEventLoopPolicy")
assert _BasePolicy is asyncio.events._BaseDefaultEventLoopPolicy
assert not hasattr(asyncio, "AbstractChildWatcher")
assert _ChildWatcherBase is object


@pytest.mark.trio
async def test_open_loop_still_works():
async with trio_asyncio.open_loop() as loop:
assert asyncio.get_running_loop() is loop
75 changes: 63 additions & 12 deletions trio_asyncio/_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,42 @@ def _in_trio_context_other_than_sync_loop():
return False


class _TrioPolicy(asyncio.events.BaseDefaultEventLoopPolicy):
# Compatibility shims for asyncio API changes across Python versions:
# - 3.14 renamed BaseDefaultEventLoopPolicy -> _BaseDefaultEventLoopPolicy
# (https://github.com/python/cpython/pull/128216); see issue #160.
# - 3.14 removed AbstractChildWatcher (deprecated since 3.12); see issue #152.
# - 3.14 deprecated the public policy helpers and prefers underscored names.
try:
_BasePolicy = asyncio.events.BaseDefaultEventLoopPolicy
except AttributeError: # pragma: no cover
_BasePolicy = asyncio.events._BaseDefaultEventLoopPolicy

try:
if sys.platform == "win32": # pragma: no cover
from asyncio.windows_events import (
_DefaultEventLoopPolicy as _PlatformDefaultPolicy,
)
else:
from asyncio.unix_events import (
_DefaultEventLoopPolicy as _PlatformDefaultPolicy,
)
except ImportError:
# Python < 3.14 exports the public name only.
_PlatformDefaultPolicy = asyncio.DefaultEventLoopPolicy

try:
_AbstractEventLoopPolicy = asyncio.events._AbstractEventLoopPolicy
except AttributeError:
_AbstractEventLoopPolicy = asyncio.AbstractEventLoopPolicy

if sys.platform == "win32": # pragma: no cover
_ChildWatcherBase = object
else:
# Falls back to object once AbstractChildWatcher is removed (3.14+).
_ChildWatcherBase = getattr(asyncio, "AbstractChildWatcher", object)


class _TrioPolicy(_BasePolicy):
@staticmethod
def _loop_factory():
raise RuntimeError("Event loop creations shouldn't get here")
Expand Down Expand Up @@ -222,9 +257,7 @@ def _new_policy_set(new_policy):
raise RuntimeError("You can't set the Trio loop policy manually")
if _in_trio_context_other_than_sync_loop():
raise RuntimeError("You can't change the event loop policy in Trio context")
if new_policy is not None and not isinstance(
new_policy, asyncio.AbstractEventLoopPolicy
):
if new_policy is not None and not isinstance(new_policy, _AbstractEventLoopPolicy):
# Raise the type of error that the CPython test suite expects
raise_type = TypeError if sys.version_info >= (3, 11) else AssertionError
raise raise_type(
Expand All @@ -234,12 +267,21 @@ def _new_policy_set(new_policy):
_faked_policy.policy = new_policy


_orig_policy_get = _aio_event.get_event_loop_policy
_orig_policy_set = _aio_event.set_event_loop_policy
# Prefer the non-deprecated private helpers on 3.14+ so import-time policy
# install does not emit DeprecationWarning under filterwarnings=error.
_orig_policy_get = getattr(
_aio_event, "_get_event_loop_policy", _aio_event.get_event_loop_policy
)
_orig_policy_set = getattr(
_aio_event, "_set_event_loop_policy", _aio_event.set_event_loop_policy
)
_aio_event.get_event_loop_policy = _new_policy_get
_aio_event.set_event_loop_policy = _new_policy_set
asyncio.get_event_loop_policy = _new_policy_get
asyncio.set_event_loop_policy = _new_policy_set
if hasattr(_aio_event, "_get_event_loop_policy"):
_aio_event._get_event_loop_policy = _new_policy_get
_aio_event._set_event_loop_policy = _new_policy_set

#####

Expand Down Expand Up @@ -317,10 +359,13 @@ def _new_current_task(loop=None):
#####


class TrioPolicy(_TrioPolicy, asyncio.DefaultEventLoopPolicy):
class TrioPolicy(_TrioPolicy, _PlatformDefaultPolicy):
"""This is the loop policy that's active whenever we're in a Trio context."""

def _init_watcher(self):
# 3.14+ DefaultEventLoopPolicy no longer owns a child watcher.
if not hasattr(self, "_watcher"):
self._watcher = None
with asyncio.events._lock:
if self._watcher is None: # pragma: no branch
self._watcher = TrioChildWatcher()
Expand All @@ -343,7 +388,15 @@ def set_child_watcher(self, watcher):
watcher.close()
watcher = TrioChildWatcher()
watcher.attach_loop(loop)
super().set_child_watcher(watcher)
parent_set = getattr(super(), "set_child_watcher", None)
if parent_set is not None:
parent_set(watcher)
else:
# Child-watcher policy APIs were removed in Python 3.14.
old = getattr(self, "_watcher", None)
if old is not None:
old.close()
self._watcher = watcher


_original_policy = _orig_policy_get()
Expand All @@ -356,9 +409,7 @@ def set_child_watcher(self, watcher):
current_loop = ContextVar("trio_aio_loop", default=None)


class TrioChildWatcher(
asyncio.AbstractChildWatcher if sys.platform != "win32" else object
):
class TrioChildWatcher(_ChildWatcherBase):
"""Watches for child processes to exit using Trio APIs.

All TrioChildWatchers behave identically, so there's no reason to construct
Expand All @@ -367,7 +418,7 @@ class TrioChildWatcher(
``asyncio.get_event_loop_policy
"""

# AbstractChildWatcher not available under Windows
# AbstractChildWatcher is unavailable on Windows and removed in 3.14+
def __init__(self):
super().__init__()
self._callbacks = {} # pid => handler
Expand Down
Loading