From 5db6221aa84140c793d146bf1050a4c325092dac Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Thu, 11 Jan 2024 18:04:18 -0500 Subject: [PATCH 1/5] Use greenlets rather than threads for sync loop --- tests/conftest.py | 14 --- trio_asyncio/_base.py | 85 ++++++++--------- trio_asyncio/_handles.py | 2 +- trio_asyncio/_loop.py | 22 +++-- trio_asyncio/_sync.py | 192 +++++++++++++++++++-------------------- 5 files changed, 149 insertions(+), 166 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2085cd2..8b4744c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -123,10 +123,6 @@ def xfail(rel_id): def skip(rel_id): mark(pytest.mark.skip, rel_id) - # This hangs, probably due to the thread shenanigans (it works - # fine with a greenlet-based sync loop) - skip("test_base_events.py::RunningLoopTests::test_running_loop_within_a_loop") - # Remainder of these have unclear issues if sys.version_info < (3, 8): xfail( @@ -201,17 +197,7 @@ def skip(rel_id): may_be_absent=True, ) - if sys.version_info >= (3, 9): - # This tries to create a new loop from within an existing one, - # which we don't support. - xfail("test_locks.py::ConditionTests::test_ambiguous_loops") - if sys.version_info >= (3, 12): - # This test sets signal handlers from within a coroutine, - # which doesn't work for us because SyncTrioEventLoop runs on - # a non-main thread. - xfail("test_unix_events.py::TestFork::test_fork_signal_handling") - # This test explicitly uses asyncio.tasks._c_current_task, # bypassing our monkeypatch. xfail( diff --git a/trio_asyncio/_base.py b/trio_asyncio/_base.py index df9b30d..f750832 100644 --- a/trio_asyncio/_base.py +++ b/trio_asyncio/_base.py @@ -492,34 +492,34 @@ def add_reader(self, fd, callback, *args): self._ensure_fd_no_transport(fd) return self._add_reader(fd, callback, *args) - def _add_reader(self, fd, callback, *args): + # Local helper to factor out common logic between _add_reader/_add_writer + def _add_io_handler(self, set_handle, wait_ready, fd, callback, args): self._check_closed() handle = ScopedHandle(callback, args, self) - reader = self._set_read_handle(fd, handle) - if reader is not None: - reader.cancel() - if self._token is None: - return - self._nursery.start_soon(self._reader_loop, fd, handle) + old_handle = set_handle(fd, handle) - def _set_read_handle(self, fd, handle): - try: - key = self._selector.get_key(fd) - except KeyError: - self._selector.register(fd, EVENT_READ, (handle, None)) + if old_handle is not None: + old_handle.cancel() + if self._token is None: return None - else: - mask, (reader, writer) = key.events, key.data - self._selector.modify(fd, mask | EVENT_READ, (handle, writer)) - return reader + self._nursery.start_soon(self._io_task, fd, handle, wait_ready) + return handle - async def _reader_loop(self, fd, handle): + async def _io_task(self, fd, handle, wait_ready): with handle._scope: try: while True: if handle._cancelled: break - await _wait_readable(fd) + try: + await wait_ready(fd) + except OSError: + # maybe someone did + # h = add_reader(sock); h.cancel(); sock.close() + # without yielding to the event loop + if handle._cancelled: + break + raise if handle._cancelled: break handle._run() @@ -527,6 +527,22 @@ async def _reader_loop(self, fd, handle): except Exception as exc: handle._raise(exc) + def _add_reader(self, fd, callback, *args): + return self._add_io_handler( + self._set_read_handle, _wait_readable, fd, callback, args + ) + + def _set_read_handle(self, fd, handle): + try: + key = self._selector.get_key(fd) + except KeyError: + self._selector.register(fd, EVENT_READ, (handle, None)) + return None + else: + mask, (reader, writer) = key.events, key.data + self._selector.modify(fd, mask | EVENT_READ, (handle, writer)) + return reader + # writing to a file descriptor def add_writer(self, fd, callback, *args): @@ -546,15 +562,10 @@ def add_writer(self, fd, callback, *args): # remove_writer: unchanged from asyncio - def _add_writer(self, fd, callback, *args): - self._check_closed() - handle = ScopedHandle(callback, args, self) - writer = self._set_write_handle(fd, handle) - if writer is not None: - writer.cancel() - if self._token is None: - return - self._nursery.start_soon(self._writer_loop, fd, handle) + def _add_writer(self, fd, callback, *args, _defer_start=False): + return self._add_io_handler( + self._set_write_handle, _wait_writable, fd, callback, args + ) def _set_write_handle(self, fd, handle): try: @@ -566,20 +577,6 @@ def _set_write_handle(self, fd, handle): self._selector.modify(fd, mask | EVENT_WRITE, (reader, handle)) return writer - async def _writer_loop(self, fd, handle): - with handle._scope: - try: - while True: - if handle._cancelled: - break - await _wait_writable(fd) - if handle._cancelled: - break - handle._run() - await self.synchronize() - except Exception as exc: - handle._raise(exc) - def autoclose(self, fd): """ Mark a file descriptor so that it's auto-closed along with this loop. @@ -752,6 +749,7 @@ async def _main_loop_exit(self): # clean core fields self._nursery = None self._task = None + self._token = None def is_running(self): if self._stopped is None: @@ -778,6 +776,11 @@ async def wait_stopped(self): """ await self._stopped.wait() + def _trio_io_cancel(self, cancel_scope): + """Called when a ScopedHandle representing an I/O reader or writer + has its cancel() method called.""" + cancel_scope.cancel() + def stop(self): """Halt the main loop. diff --git a/trio_asyncio/_handles.py b/trio_asyncio/_handles.py index 69a68d3..0426987 100644 --- a/trio_asyncio/_handles.py +++ b/trio_asyncio/_handles.py @@ -31,7 +31,7 @@ def __init__(self, *args, **kw): def cancel(self): super().cancel() - self._scope.cancel() + self._loop._trio_io_cancel(self._scope) def _repr_info(self): return super()._repr_info() + ["scope={!r}".format(self._scope)] diff --git a/trio_asyncio/_loop.py b/trio_asyncio/_loop.py index 3bf4bb5..a1435fb 100644 --- a/trio_asyncio/_loop.py +++ b/trio_asyncio/_loop.py @@ -11,7 +11,6 @@ from ._async import TrioEventLoop from ._util import run_aio_future -from ._deprecate import warn_deprecated try: from trio.lowlevel import wait_for_child @@ -106,26 +105,29 @@ def _in_trio_context(): return True +_sync_loop_task_name = "trio_asyncio sync loop task" + + +def _in_trio_context_other_than_sync_loop(): + try: + return trio.lowlevel.current_task().name != _sync_loop_task_name + except RuntimeError: + return False + + class _TrioPolicy(asyncio.events.BaseDefaultEventLoopPolicy): @staticmethod def _loop_factory(): raise RuntimeError("Event loop creations shouldn't get here") def new_event_loop(self): - if _in_trio_context(): + if _in_trio_context_other_than_sync_loop(): raise RuntimeError( "You're within a Trio environment.\n" "Use 'async with open_loop()' instead." ) if _faked_policy.policy is not None: return _faked_policy.policy.new_event_loop() - if "pytest" not in sys.modules: - warn_deprecated( - "Using trio-asyncio outside of a Trio event loop", - "0.10.0", - issue=None, - instead=None, - ) from ._sync import SyncTrioEventLoop @@ -220,7 +222,7 @@ def _new_policy_get(): def _new_policy_set(new_policy): if isinstance(new_policy, TrioPolicy): raise RuntimeError("You can't set the Trio loop policy manually") - if _in_trio_context(): + 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 diff --git a/trio_asyncio/_sync.py b/trio_asyncio/_sync.py index a7cb2f3..7de5823 100644 --- a/trio_asyncio/_sync.py +++ b/trio_asyncio/_sync.py @@ -2,17 +2,37 @@ import trio import queue +import signal import asyncio -import threading import outcome +import greenlet +import contextlib from ._base import BaseTrioEventLoop, TrioAsyncioExit +from ._loop import current_loop async def _sync(proc, *args): return proc(*args) +# Context manager to ensure all between-greenlet switches occur with +# an empty trio run context and unset signal wakeup fd. That way, each +# greenlet can have its own private Trio run. +@contextlib.contextmanager +def clean_trio_state(): + trio_globals = trio._core._run.GLOBAL_RUN_CONTEXT.__dict__ + old_state = trio_globals.copy() + old_wakeup_fd = signal.set_wakeup_fd(-1) + trio_globals.clear() + try: + yield + finally: + signal.set_wakeup_fd(old_wakeup_fd, warn_on_full_buffer=(not old_state)) + trio_globals.clear() + trio_globals.update(old_state) + + class SyncTrioEventLoop(BaseTrioEventLoop): """ This is the "compatibility mode" implementation of the Trio/asyncio @@ -23,21 +43,18 @@ class SyncTrioEventLoop(BaseTrioEventLoop): :class:`trio_asyncio.TrioEventLoop` – if possible. """ - _thread = None - _thread_running = False + _loop_running = False _stop_pending = False + _glet = None - def __init__(self, **kw): - super().__init__(**kw) - - # for sync operation - self.__blocking_job_queue = queue.Queue() - self.__blocking_result_queue = queue.Queue() - - # Synchronization - self._some_deferred = 0 + def __init__(self, *args, **kwds): + super().__init__(*args, **kwds) - self._start_loop() + # We must start the Trio loop immediately so that self.time() works + self._glet = greenlet.greenlet(trio.run) + with clean_trio_state(): + if not self._glet.switch(self.__trio_main): + raise RuntimeError("Loop could not be started") def stop(self): """Halt the main loop. @@ -51,64 +68,43 @@ def do_stop(): self._stop_pending = False raise TrioAsyncioExit("stopping trio-asyncio loop") - # async def stop_me(): - # def kick_(): - # raise StopAsyncIteration - # self._queue_handle(asyncio.Handle(kick_, (), self)) - # await self._main_loop() - # if threading.current_thread() != self._thread: - # self.__run_in_thread(stop_me) - # else: - - if self._thread_running and not self._stop_pending: + if self._loop_running and not self._stop_pending: self._stop_pending = True self._queue_handle(asyncio.Handle(do_stop, (), self)) def _queue_handle(self, handle): self._check_closed() - - def put(self, handle): - self._some_deferred -= 1 - self._q_send.send_nowait(handle) - - # If we don't have a token, the main loop is not yet running - # thus we can't have a race condition. - # - # On the other hand, if a request has been submitted (but not yet - # processed) through self._token, any other requestss also must be - # sent that way, otherwise they'd overtake each other. - if self._token is not None and ( - self._some_deferred or threading.current_thread() != self._thread - ): - self._some_deferred += 1 - self._token.run_sync_soon(put, self, handle) + if self._glet is not greenlet.getcurrent() and self._token is not None: + self.__run_in_greenlet(_sync, self._q_send.send_nowait, handle) else: self._q_send.send_nowait(handle) return handle def run_forever(self): - if self._thread == threading.current_thread(): - raise RuntimeError( - "You can't nest calls to run_until_complete()/run_forever()." - ) - self.__run_in_thread(self._main_loop) + self.__run_in_greenlet(self._main_loop) def is_running(self): if self._closed: return False - return self._thread_running + return self._loop_running def _add_reader(self, fd, callback, *args): - if self._thread is None or self._thread == threading.current_thread(): - super()._add_reader(fd, callback, *args) + if self._glet is not greenlet.getcurrent() and self._token is not None: + self.__run_in_greenlet(_sync, super()._add_reader, fd, callback, *args) else: - self.__run_in_thread(_sync, super()._add_reader, fd, callback, *args) + super()._add_reader(fd, callback, *args) def _add_writer(self, fd, callback, *args): - if self._thread is None or self._thread == threading.current_thread(): + if self._glet is not greenlet.getcurrent() and self._token is not None: + self.__run_in_greenlet(_sync, super()._add_writer, fd, callback, *args) + else: super()._add_writer(fd, callback, *args) + + def _trio_io_cancel(self, cancel_scope): + if self._glet is not greenlet.getcurrent() and self._token is not None: + self.__run_in_greenlet(_sync, cancel_scope.cancel) else: - self.__run_in_thread(_sync, super()._add_writer, fd, callback, *args) + cancel_scope.cancel() def run_until_complete(self, future): """Run until the Future is done. @@ -121,12 +117,7 @@ def run_until_complete(self, future): Return the Future's result, or raise its exception. """ - - if self._thread == threading.current_thread(): - raise RuntimeError( - "You can't nest calls to run_until_complete()/run_forever()." - ) - return self.__run_in_thread(self._run_coroutine, future) + return self.__run_in_greenlet(self._run_coroutine, future) async def _run_coroutine(self, future): """Helper for run_until_complete(). @@ -134,7 +125,7 @@ async def _run_coroutine(self, future): We need to make sure that a RuntimeError is raised if the loop is stopped before the future completes. - This code runs in the Trio thread. + This code runs in the Trio greenlet. """ result = None future = asyncio.ensure_future(future, loop=self) @@ -163,83 +154,84 @@ def is_done(_): raise RuntimeError("Event loop stopped before Future completed.") return result.unwrap() - def __run_in_thread(self, async_fn, *args): + def __run_in_greenlet(self, async_fn, *args): self._check_closed() - if self._thread is None: + if self._loop_running: raise RuntimeError( - "You need to wrap your main code in a 'with loop:' statement." + "You can't nest calls to run_until_complete()/run_forever()." ) - if not self._thread.is_alive(): - raise RuntimeError("The Trio thread is not running") - self.__blocking_job_queue.put((async_fn, args)) - res = self.__blocking_result_queue.get() + if asyncio._get_running_loop() is not None: + raise RuntimeError( + "Cannot run the event loop while another loop is running" + ) + if not self._glet: + if async_fn is _sync: + # Allow for cleanups during close() + sync_fn, *args = args + return sync_fn(*args) + raise RuntimeError("The Trio greenlet is not running") + with clean_trio_state(): + res = self._glet.switch((greenlet.getcurrent(), async_fn, args)) if res is None: raise RuntimeError("Loop has died / terminated") return res.unwrap() - def _start_loop(self): - self._check_closed() + async def __trio_main(self): + from ._loop import _sync_loop_task_name - if self._thread is None: - self._thread = threading.Thread( - name="trio-asyncio-" + threading.current_thread().name, - target=trio.run, - daemon=True, - args=(self.__trio_thread_main,), - ) - self._thread.start() - x = self.__blocking_result_queue.get() - if x is not True: - raise RuntimeError("Loop could not be started", x) + trio.lowlevel.current_task().name = _sync_loop_task_name - async def __trio_thread_main(self): # The non-context-manager equivalent of open_loop() async with trio.open_nursery() as nursery: - asyncio.set_event_loop(self) await self._main_loop_init(nursery) - self.__blocking_result_queue.put(True) + with clean_trio_state(): + req = greenlet.getcurrent().parent.switch(True) while not self._closed: - # This *blocks* - req = self.__blocking_job_queue.get() if req is None: break - async_fn, args = req + caller, async_fn, args = req - self._thread_running = True + self._loop_running = True + asyncio._set_running_loop(self) + current_loop.set(self) result = await outcome.acapture(async_fn, *args) - self._thread_running = False - if ( - type(result) == outcome.Error - and type(result.error) == trio.Cancelled + asyncio._set_running_loop(None) + current_loop.set(None) + self._loop_running = False + + if isinstance(result, outcome.Error) and isinstance( + result.error, trio.Cancelled ): res = RuntimeError("Main loop cancelled") res.__cause__ = result.error.__cause__ result = outcome.Error(res) - self.__blocking_result_queue.put(result) + + with clean_trio_state(): + req = caller.switch(result) + with trio.CancelScope(shield=True): await self._main_loop_exit() - self.__blocking_result_queue.put(None) nursery.cancel_scope.cancel() def __enter__(self): - # I'd like to enforce this, but … no way - # if self._thread is not None: - # raise RuntimeError("This loop is already running.") - # self._start_loop() return self def __exit__(self, *tb): self.stop() self.close() - assert self._thread is None + assert self._glet is None def _close(self): """Hook to terminate the thread""" - if self._thread is not None: - if self._thread == threading.current_thread(): + if self._glet is not None: + if self._glet is greenlet.getcurrent(): raise RuntimeError("You can't close a sync loop from the inside") - self.__blocking_job_queue.put(None) - self._thread.join() - self._thread = None + # The parent will generally already be this greenlet, but might + # not be in nested-loop cases. + self._glet.parent = greenlet.getcurrent() + with clean_trio_state(): + self._glet.switch(None) + assert self._glet.dead + self._glet = None super()._close() From cedaef35788846c846198631573e27d70cebd131 Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Mon, 29 Jan 2024 21:46:46 -0700 Subject: [PATCH 2/5] Add greenlet dependency --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 2f3557e..eee285a 100644 --- a/setup.py +++ b/setup.py @@ -67,6 +67,7 @@ "outcome", "sniffio >= 1.3.0", "exceptiongroup >= 1.0.0; python_version < '3.11'", + "greenlet", ], # This means, just install *everything* you see under trio/, even if it # doesn't look like a source file, so long as it appears in MANIFEST.in: From fbaec7b4343c3f0655ee924354898bf5ba6d59f6 Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Tue, 30 Jan 2024 00:02:58 -0700 Subject: [PATCH 3/5] CI fixes, newsfragment, doc updates --- docs-requirements.txt | 1 + docs/source/usage.rst | 37 ++++++++++++----------------------- newsfragments/137.feature.rst | 10 ++++++++++ tests/conftest.py | 6 ------ trio_asyncio/_sync.py | 9 +++++++-- 5 files changed, 31 insertions(+), 32 deletions(-) create mode 100644 newsfragments/137.feature.rst diff --git a/docs-requirements.txt b/docs-requirements.txt index f63de0f..1117a9a 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -5,3 +5,4 @@ towncrier trio >= 0.15.0 outcome attrs +greenlet diff --git a/docs/source/usage.rst b/docs/source/usage.rst index dedb125..4776adf 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -104,7 +104,7 @@ Asyncio main loop +++++++++++++++++ Sometimes you instead start with asyncio code which you wish to extend -with some Trio portions. By far the best-supported approach here is to +with some Trio portions. The best-supported approach here is to wrap your entire asyncio program in a Trio event loop. In other words, you should transform this code:: @@ -123,27 +123,18 @@ to this:: trio_asyncio.run(trio_asyncio.aio_as_trio(async_main)) If your program makes multiple calls to ``run_until_complete()`` and/or -``run_forever()``, this may be a somewhat challenging transformation. -In theory, you can instead keep the old approach (``get_event_loop()`` + +``run_forever()``, or if the call to :func:`asyncio.run` is hidden inside +a library you're using, then this may be a somewhat challenging transformation. +In such cases, you can instead keep the old approach (``get_event_loop()`` + ``run_until_complete()``) unchanged, and if you've imported ``trio_asyncio`` (and not changed the asyncio event loop policy) you'll still be able to use :func:`~trio_asyncio.trio_as_aio` to run Trio code from within your -asyncio-flavored functions. In practice, this is not recommended, because: - -* It's implemented by running the contents of the loop in an - additional thread, so anything that expects to run on the main - thread (such as a signal handler) won't be happy. - -* The implementation is kind of a terrible hack. - -For these reasons, obtaining a new Trio-enabled asyncio event loop -using the standard asyncio functions (:func:`asyncio.get_event_loop`, -etc), rather than :func:`trio_asyncio.open_loop`, will raise a -deprecation warning. (Except when running under pytest, because -support for ``run_until_complete()`` is often needed to test asyncio -libraries' test suites against trio-asyncio.) asyncio is transitioning -towards the model of using a single top-level :func:`asyncio.run` call -anyway, so the effort you spend on conversion won't be wasted. +asyncio-flavored functions. This is referred to internally as a "sync loop" +(``SyncTripEventLoop``), as contrasted with the "async loop" that you use +when you start from an existing Trio run. The sync loop is implemented using +the `greenlet` library to switch out of a Trio run that has not yet completed, +so it is less well-supported than the approach where you start in Trio. +But as of trio-asyncio 0.14.0, we do think it should generally work. Compatibility issues ++++++++++++++++++++ @@ -166,7 +157,7 @@ Interrupting the asyncio loop A trio-asyncio event loop created with :func:`open_loop` does not support ``run_until_complete`` or ``run_forever``. If you need these features, -you might be able to get away with using a (deprecated) "sync loop" as +you might be able to get away with using a "sync loop" as explained :ref:`above `, but it's better to refactor your program so all of its async code runs within a single event loop invocation. For example, you might replace:: @@ -180,7 +171,7 @@ invocation. For example, you might replace:: loop = asyncio.get_event_loop() loop.run_until_complete(setup) loop.run_forever() - + with:: stopped_event = trio.Event() @@ -202,9 +193,7 @@ Detecting the current function's flavor :func:`sniffio.current_async_library` correctly reports "asyncio" or "trio" when called from a trio-asyncio program, based on the flavor of -function that's calling it. (Some corner cases -might not work on Pythons below 3.7 where asyncio doesn't support -context variables.) +function that's calling it. However, this feature should generally not be necessary, because you should know whether each function in your program is asyncio-flavored diff --git a/newsfragments/137.feature.rst b/newsfragments/137.feature.rst new file mode 100644 index 0000000..aba6890 --- /dev/null +++ b/newsfragments/137.feature.rst @@ -0,0 +1,10 @@ +trio-asyncio now implements its :ref:`synchronous event loop ` +(which is used when the top-level of your program is an asyncio call such as +:func:`asyncio.run`, rather than a Trio call such as :func:`trio.run`) +using the ``greenlet`` library rather than a separate thread. This provides +some better theoretical grounding and fixes various edge cases around signal +handling and other integrations; in particular, recent versions of IPython +will no longer crash when importing trio-asyncio. Synchronous event loops have +been un-deprecated with this change, though we still recommend using an +async loop (``async with trio_asyncio.open_loop():`` from inside a Trio run) +where possible. diff --git a/tests/conftest.py b/tests/conftest.py index 8b4744c..f72bb76 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -198,12 +198,6 @@ def skip(rel_id): ) if sys.version_info >= (3, 12): - # This test explicitly uses asyncio.tasks._c_current_task, - # bypassing our monkeypatch. - xfail( - "test_tasks.py::CCurrentLoopTests::test_current_task_with_implicit_loop" - ) - # These tests assume asyncio.sleep(0) is sufficient to run all pending tasks xfail( "test_futures2.py::PyFutureTests::test_task_exc_handler_correct_context" diff --git a/trio_asyncio/_sync.py b/trio_asyncio/_sync.py index 7de5823..0dfe775 100644 --- a/trio_asyncio/_sync.py +++ b/trio_asyncio/_sync.py @@ -23,12 +23,17 @@ async def _sync(proc, *args): def clean_trio_state(): trio_globals = trio._core._run.GLOBAL_RUN_CONTEXT.__dict__ old_state = trio_globals.copy() - old_wakeup_fd = signal.set_wakeup_fd(-1) + old_wakeup_fd = None + try: + old_wakeup_fd = signal.set_wakeup_fd(-1) + except ValueError: + pass # probably we're on the non-main thread trio_globals.clear() try: yield finally: - signal.set_wakeup_fd(old_wakeup_fd, warn_on_full_buffer=(not old_state)) + if old_wakeup_fd is not None: + signal.set_wakeup_fd(old_wakeup_fd, warn_on_full_buffer=(not old_state)) trio_globals.clear() trio_globals.update(old_state) From 12fc93c3ffc0b00a56bbf05343b6ca89a7d28e74 Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Tue, 30 Jan 2024 00:05:38 -0700 Subject: [PATCH 4/5] Fix doc cross-reference --- docs/source/usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 4776adf..d1328c6 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -132,7 +132,7 @@ In such cases, you can instead keep the old approach (``get_event_loop()`` + asyncio-flavored functions. This is referred to internally as a "sync loop" (``SyncTripEventLoop``), as contrasted with the "async loop" that you use when you start from an existing Trio run. The sync loop is implemented using -the `greenlet` library to switch out of a Trio run that has not yet completed, +the ``greenlet`` library to switch out of a Trio run that has not yet completed, so it is less well-supported than the approach where you start in Trio. But as of trio-asyncio 0.14.0, we do think it should generally work. From b0625e8cd42ade1d0186a0783e73756623e9fc3e Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Tue, 30 Jan 2024 00:08:25 -0700 Subject: [PATCH 5/5] Clarify deprecation status --- docs/source/principles.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/source/principles.rst b/docs/source/principles.rst index 9094435..9f26a65 100644 --- a/docs/source/principles.rst +++ b/docs/source/principles.rst @@ -168,10 +168,11 @@ this gap by providing two event loop implementations. installed a custom event loop policy, calling :func:`asyncio.new_event_loop` (including the implicit call made by the first :func:`asyncio.get_event_loop` in the main thread) will give you an event loop that transparently runs - in a separate thread in order to support multiple + in a separate greenlet in order to support multiple calls to :meth:`~asyncio.loop.run_until_complete`, :meth:`~asyncio.loop.run_forever`, and :meth:`~asyncio.loop.stop`. Sync loops are intended to allow trio-asyncio to run the existing test suites of large asyncio libraries, which often call :meth:`~asyncio.loop.run_until_complete` on the same loop multiple times. - Using them for other purposes is deprecated. + Using them for other purposes is not recommended (it is better to refactor + so you can use an async loop) but will probably work.