diff --git a/docs/source/index.rst b/docs/source/index.rst index 93227db..e06a044 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -58,48 +58,60 @@ avoid collisions, this should match your library's PEP 503 normalized name on Py .. data:: thread_local.name -Make sure that whenever your library is calling a coroutine ``throw()``, ``send()``, or ``close()`` -that this is set to your identifier string. In most cases, this will be as simple as: +Make sure that whenever your library is potentially executing user-provided code, +this is set to your identifier string. In many cases, you can set it once when +your library starts up and restore it on shutdown: .. code-block:: python3 - from sniffio import thread_local - - # Your library's step function - def step(...): - old_name, thread_local.name = thread_local.name, "my-library's-PyPI-name" - try: - result = coro.send(None) - finally: - thread_local.name = old_name + from sniffio import thread_local as sniffio_loop + + # Your library's run function (like trio.run() or asyncio.run()) + def run(...): + old_name, sniffio_loop.name = sniffio_loop.name, "my-library's-PyPI-name" + try: + # actual event loop implementation left as an exercise to the reader + finally: + sniffio_loop.name = old_name + +In unusual situations you may need to be more fine-grained about it: + +* If you're using something akin to Trio `guest mode + `__ + to permit running your library on top of another event loop, then + you'll want to make sure that :func:`current_async_library` can + correctly identify which library (host or guest) is running at any + given moment. To achieve this, you should set and restore + :data:`thread_local.name` around each "tick" of your library's logic + (the part that is invoked as a callback from the host loop), rather + than around an entire ``run()`` function. + +* If you're using something akin to `trio-asyncio + `__ to implement one async + library on top of another, then you can set and restore :data:`thread_local.name` + around each task step (call to a coroutine object ``send()``, ``throw()``, or + ``close()`` method) into the 'inner' library. For example, trio-asyncio does + something like: + + .. code-block:: python3 + + from sniffio import thread_local as sniffio_loop + + # Your library's compatibility loop + async def main_loop(self, ...) -> None: + ... + handle: asyncio.Handle = await self.get_next_handle() + old_name, sniffio_loop.name = sniffio_loop.name, "asyncio" + try: + result = handle._callback(obj._args) + finally: + sniffio_loop.name = old_name **Step 3:** Send us a PR to add your library to the list of supported libraries above. That's it! -There are libraries that directly drive a sniffio-naive coroutine from another, -outer sniffio-aware coroutine such as `trio_asyncio`. -These libraries should make sure to set the correct value -while calling a synchronous function that will go on to drive the -sniffio-naive coroutine. - - -.. code-block:: python3 - - from sniffio import thread_local - - # Your library's compatibility loop - async def main_loop(self, ...) -> None: - ... - handle: asyncio.Handle = await self.get_next_handle() - old_name, thread_local.name = thread_local.name, "asyncio" - try: - result = handle._callback(obj._args) - finally: - thread_local.name = old_name - - .. toctree:: :maxdepth: 1 diff --git a/newsfragments/39.feature.rst b/newsfragments/39.feature.rst new file mode 100644 index 0000000..b541b90 --- /dev/null +++ b/newsfragments/39.feature.rst @@ -0,0 +1,13 @@ +sniffio now attempts to return the expected library name when +:func:`sniffio.current_async_library` is called from code that is +associated with an async library but is not part of an async task. +This includes asyncio ``call_soon()`` and ``call_later()`` callbacks, and +Trio instrumentation and ``abort_fn`` handlers. (Previously, sniffio's +behavior in these situations was inconsistent.) The sniffio +documentation now explains more precisely which async library counts +as "currently running" in ambiguous cases. Libraries other than +asyncio may need updates to their sniffio integration in order to +fully conform to the new semantics; the documentation includes an +updated recipe. The new semantics also reduce the number of situations +where updates to sniffio's internals are required, which should modestly +improve the performance of libraries that interoperate with sniffio. diff --git a/sniffio/_impl.py b/sniffio/_impl.py index c1a7bbf..60c29c5 100644 --- a/sniffio/_impl.py +++ b/sniffio/_impl.py @@ -37,6 +37,23 @@ def current_async_library() -> str: depending on current mode ================ =========== ============================ + If :func:`current_async_library` returns ``"someio"``, then that + generally means you can ``await someio.sleep(0)`` if you're in an + async function, and you can access ``someio``\\'s global state (to + start background tasks, determine the current time, etc) even if you're + not in an async function. + + .. note:: Situations such as `guest mode + `__ + and `trio-asyncio `__ + can result in more than one async library being "running" in the same + thread at the same time. In such ambiguous cases, `sniffio` + returns the name of the library that has most directly invoked its + caller. Within an async task, if :func:`current_async_library` + returns ``"someio"`` then that means you can ``await someio.sleep(0)``. + Outside of a task, you will get ``"asyncio"`` in asyncio callbacks, + ``"trio"`` in trio instruments and abort handlers, etc. + Returns: A string like ``"trio"``. @@ -75,11 +92,13 @@ async def generic_sleep(seconds): if "asyncio" in sys.modules: import asyncio try: - current_task = asyncio.current_task # type: ignore[attr-defined] + test = asyncio._get_running_loop # type: ignore[attr-defined] except AttributeError: - current_task = asyncio.Task.current_task # type: ignore[attr-defined] + # 3.6 doesn't have _get_running_loop, so we can only detect + # asyncio if we're inside a task (as opposed to a callback) + test = asyncio.Task.current_task # type: ignore[attr-defined] try: - if current_task() is not None: + if test() is not None: return "asyncio" except RuntimeError: pass