Skip to content

Commit 7a51a8c

Browse files
committed
Fixes for 3.12 and for a few hangs in specific other environments
1 parent 4a11c51 commit 7a51a8c

File tree

6 files changed

+122
-61
lines changed

6 files changed

+122
-61
lines changed

tests/python/conftest.py

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,11 @@ def skip(rel_id):
115115
"test_log_slow_callbacks"
116116
)
117117

118-
xfail(
119-
"test_tasks.py::RunCoroutineThreadsafeTests::"
120-
"test_run_coroutine_threadsafe_task_factory_exception"
121-
)
118+
if sys.version_info < (3, 12):
119+
xfail(
120+
"test_tasks.py::RunCoroutineThreadsafeTests::"
121+
"test_run_coroutine_threadsafe_task_factory_exception"
122+
)
122123
if sys.version_info >= (3, 8):
123124
xfail(
124125
"test_tasks.py::RunCoroutineThreadsafeTests::"
@@ -130,7 +131,8 @@ def skip(rel_id):
130131
"test_run_coroutine_threadsafe_with_timeout"
131132
)
132133
if sys.platform == "win32":
133-
xfail("test_windows_events.py::ProactorLoopCtrlC::test_ctrl_c")
134+
# hangs on 3.11+, fails without hanging on 3.8-3.10
135+
skip("test_windows_events.py::ProactorLoopCtrlC::test_ctrl_c")
134136

135137
if sys.implementation.name == "pypy":
136138
# This test depends on the C implementation of asyncio.Future, and
@@ -141,12 +143,6 @@ def skip(rel_id):
141143
"test_inherit_without_calling_super_init"
142144
)
143145
if sys.version_info < (3, 8):
144-
# This fails due to a trivial difference in how pypy handles IPv6
145-
# addresses
146-
xfail(
147-
"test_base_events.py::BaseEventLoopWithSelectorTests::"
148-
"test_create_connection_ipv6_scope"
149-
)
150146
# These tests assume CPython-style immediate finalization of
151147
# objects when they become unreferenced
152148
for test in (
@@ -155,6 +151,18 @@ def skip(rel_id):
155151
"test_start_tls_client_reg_proto_1",
156152
):
157153
xfail("test_sslproto.py::SelectorStartTLSTests::{}".format(test))
154+
if sys.version_info < (3, 8) or sys.version_info >= (3, 9):
155+
# This fails due to a trivial difference in how pypy handles IPv6
156+
# addresses (fails in a different way on each of 3.7 and 3.9)
157+
xfail(
158+
"test_base_events.py::BaseEventLoopWithSelectorTests::"
159+
"test_create_connection_ipv6_scope"
160+
)
161+
if sys.platform == "darwin":
162+
# https://foss.heptapod.net/pypy/pypy/-/issues/3964 causes infinite loops
163+
for nodeid, item in by_id.items():
164+
if "sendfile" in nodeid:
165+
item.add_marker(pytest.mark.skip)
158166

159167
if sys.version_info >= (3, 11):
160168
# This tries to use a mock ChildWatcher that does something unlikely.
@@ -169,3 +177,18 @@ def skip(rel_id):
169177
# This tries to create a new loop from within an existing one,
170178
# which we don't support.
171179
xfail("test_locks.py::ConditionTests::test_ambiguous_loops")
180+
181+
if sys.version_info >= (3, 12):
182+
# This test sets signal handlers from within a coroutine,
183+
# which doesn't work for us because SyncTrioEventLoop runs on
184+
# a non-main thread.
185+
xfail("test_unix_events.py::TestFork::test_fork_signal_handling")
186+
187+
# This test explicitly uses asyncio.tasks._c_current_task,
188+
# bypassing our monkeypatch.
189+
xfail("test_tasks.py::CCurrentLoopTests::test_current_task_with_implicit_loop")
190+
191+
# These tests assume asyncio.sleep(0) is sufficient to run all pending tasks
192+
xfail("test_futures2.py::PyFutureTests::test_task_exc_handler_correct_context")
193+
xfail("test_futures2.py::CFutureTests::test_task_exc_handler_correct_context")
194+

tests/test_misc.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,16 @@ class Seen:
1515
class TestMisc:
1616
@pytest.mark.trio
1717
async def test_close_no_stop(self):
18-
with pytest.raises(RuntimeError):
19-
async with trio_asyncio.open_loop() as loop:
18+
async with trio_asyncio.open_loop() as loop:
19+
triggered = trio.Event()
2020

21-
def close_no_stop():
21+
def close_no_stop():
22+
with pytest.raises(RuntimeError):
2223
loop.close()
24+
triggered.set()
2325

24-
loop.call_soon(close_no_stop)
25-
26-
await trio.sleep(0.1)
27-
await loop.wait_closed()
26+
loop.call_soon(close_no_stop)
27+
await triggered.wait()
2828

2929
@pytest.mark.trio
3030
async def test_too_many_stops(self):

trio_asyncio/_async.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import trio
22
import asyncio
33

4-
from ._base import BaseTrioEventLoop
4+
from ._base import BaseTrioEventLoop, TrioAsyncioExit
55

66

77
class TrioEventLoop(BaseTrioEventLoop):
@@ -32,17 +32,23 @@ def default_exception_handler(self, context):
3232
asynchronous loops.
3333
3434
"""
35-
# TODO: add context.get('handle') to the exception
3635

36+
# Call the original default handler so we get the full info in the log
37+
super().default_exception_handler(context)
38+
39+
# Also raise an exception so it can't go unnoticed
3740
exception = context.get('exception')
3841
if exception is None:
3942
message = context.get('message')
4043
if not message:
4144
message = 'Unhandled error in event loop'
42-
raise RuntimeError(message)
43-
else:
45+
exception = RuntimeError(message)
46+
47+
async def propagate_asyncio_error():
4448
raise exception
4549

50+
self._nursery.start_soon(propagate_asyncio_error)
51+
4652
def stop(self, waiter=None):
4753
"""Halt the main loop.
4854
@@ -64,7 +70,7 @@ def stop(self, waiter=None):
6470

6571
def stop_me():
6672
waiter.set()
67-
raise StopAsyncIteration
73+
raise TrioAsyncioExit("stopping trio-asyncio loop")
6874

6975
if self._stopped.is_set():
7076
waiter.set()

trio_asyncio/_base.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ def clear(self):
3737
pass
3838

3939

40+
# Exception raised internally to stop the main loop. Must subclass
41+
# SystemExit or KeyboardInterrupt in order to make it through various
42+
# asyncio layers.
43+
class TrioAsyncioExit(SystemExit):
44+
pass
45+
46+
4047
class _TrioSelector(_BaseSelectorImpl):
4148
"""A selector that hooks into a ``TrioEventLoop``.
4249
@@ -503,6 +510,8 @@ async def _reader_loop(self, fd, handle):
503510
with handle._scope:
504511
try:
505512
while True:
513+
if handle._cancelled:
514+
break
506515
await _wait_readable(fd)
507516
if handle._cancelled:
508517
break
@@ -554,6 +563,8 @@ async def _writer_loop(self, fd, handle):
554563
with handle._scope:
555564
try:
556565
while True:
566+
if handle._cancelled:
567+
break
557568
await _wait_writable(fd)
558569
if handle._cancelled:
559570
break
@@ -646,7 +657,7 @@ async def _main_loop(self, task_status=trio.TASK_STATUS_IGNORED):
646657
with trio.CancelScope(shield=True):
647658
while not self._stopped.is_set():
648659
await self._main_loop_one()
649-
except StopAsyncIteration:
660+
except TrioAsyncioExit:
650661
# raised by .stop_me() to interrupt the loop
651662
pass
652663
finally:
@@ -693,7 +704,7 @@ async def _main_loop_one(self, no_wait=False):
693704
obj._context.run(self._nursery.start_soon, obj._run, name=obj._callback)
694705
await obj._started.wait()
695706
else:
696-
obj._context.run(obj._callback, *obj._args)
707+
obj._run()
697708

698709
async def _main_loop_exit(self):
699710
"""Finalize the loop. It may not be re-entered."""
@@ -719,7 +730,7 @@ async def _main_loop_exit(self):
719730
await self._main_loop_one(no_wait=True)
720731
except trio.WouldBlock:
721732
break
722-
except StopAsyncIteration:
733+
except TrioAsyncioExit:
723734
pass
724735

725736
# Kill off unprocessed work

trio_asyncio/_loop.py

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# This code implements a clone of the asyncio mainloop which hooks into
22
# Trio.
33

4+
import os
45
import sys
56
import trio
67
import asyncio
@@ -181,6 +182,22 @@ def set_event_loop(self, loop):
181182
else:
182183
super().set_event_loop(loop)
183184

185+
# get_event_loop() without a running loop is deprecated in 3.12+. The logic for emitting the
186+
# DeprecationWarning walks the stack looking at module names in order to associate it with
187+
# the first caller outside asyncio. We need to pretend to be asyncio in order for that to work.
188+
if sys.version_info >= (3, 12):
189+
__name__ = "asyncio.fake.trio_asyncio._loop"
190+
191+
# Make sure we don't try to continue using the Trio loop after a fork()
192+
def _clear_state_after_fork():
193+
if _in_trio_context():
194+
from trio._core._run import GLOBAL_RUN_CONTEXT
195+
196+
del GLOBAL_RUN_CONTEXT.task
197+
del GLOBAL_RUN_CONTEXT.runner
198+
current_loop.set(None)
199+
200+
os.register_at_fork(after_in_child=_clear_state_after_fork)
184201

185202
from asyncio import events as _aio_event
186203

@@ -220,39 +237,33 @@ def _new_policy_set(new_policy):
220237

221238
#####
222239

223-
try:
224-
_orig_run_get = _aio_event._get_running_loop
225-
226-
except AttributeError:
227-
pass
228-
229-
else:
240+
_orig_run_get = _aio_event._get_running_loop
230241

231-
def _new_run_get():
232-
try:
233-
task = trio.lowlevel.current_task()
234-
except RuntimeError:
235-
pass
236-
else:
237-
# Trio context. Note: NOT current_loop.get()!
238-
# See comment in _TrioPolicy.get_event_loop().
239-
return task.context.get(current_loop)
240-
# Not Trio context
241-
return _orig_run_get()
242-
243-
# Must override the non-underscore-prefixed get_running_loop() too,
244-
# else will use the C-accelerated one which doesn't call the patched
245-
# _get_running_loop()
246-
def _new_run_get_or_throw():
247-
result = _new_run_get()
248-
if result is None:
249-
raise RuntimeError("no running event loop")
250-
return result
251-
252-
_aio_event._get_running_loop = _new_run_get
253-
_aio_event.get_running_loop = _new_run_get_or_throw
254-
asyncio._get_running_loop = _new_run_get
255-
asyncio.get_running_loop = _new_run_get_or_throw
242+
def _new_run_get():
243+
try:
244+
task = trio.lowlevel.current_task()
245+
except RuntimeError:
246+
pass
247+
else:
248+
# Trio context. Note: NOT current_loop.get()!
249+
# See comment in _TrioPolicy.get_event_loop().
250+
return task.context.get(current_loop)
251+
# Not Trio context
252+
return _orig_run_get()
253+
254+
# Must override the non-underscore-prefixed get_running_loop() too,
255+
# else will use the C-accelerated one which doesn't call the patched
256+
# _get_running_loop()
257+
def _new_run_get_or_throw():
258+
result = _new_run_get()
259+
if result is None:
260+
raise RuntimeError("no running event loop")
261+
return result
262+
263+
_aio_event._get_running_loop = _new_run_get
264+
_aio_event.get_running_loop = _new_run_get_or_throw
265+
asyncio._get_running_loop = _new_run_get
266+
asyncio.get_running_loop = _new_run_get_or_throw
256267

257268
#####
258269

@@ -282,6 +293,16 @@ def _new_loop_new():
282293
asyncio.set_event_loop = _new_loop_set
283294
asyncio.new_event_loop = _new_loop_new
284295

296+
# current_task is implemented in C in 3.12+, which creates a problem because it
297+
# accesses the non-monkeypatched version of _get_running_loop()
298+
from asyncio import current_task as _orig_current_task
299+
300+
def _new_current_task(loop=None):
301+
return _orig_current_task(loop or _new_run_get())
302+
303+
asyncio.tasks.current_task = _new_current_task
304+
asyncio.current_task = _new_current_task
305+
285306
#####
286307

287308

trio_asyncio/_sync.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import threading
77
import outcome
88

9-
from ._base import BaseTrioEventLoop
9+
from ._base import BaseTrioEventLoop, TrioAsyncioExit
1010

1111

1212
async def _sync(proc, *args):
@@ -49,7 +49,7 @@ def stop(self):
4949

5050
def do_stop():
5151
self._stop_pending = False
52-
raise StopAsyncIteration
52+
raise TrioAsyncioExit
5353

5454

5555
# async def stop_me():
@@ -148,7 +148,7 @@ def is_done(_):
148148
while result is None:
149149
try:
150150
await self._main_loop_one(no_wait=True)
151-
except StopAsyncIteration:
151+
except TrioAsyncioExit:
152152
pass
153153
except trio.WouldBlock:
154154
pass

0 commit comments

Comments
 (0)