Skip to content

Commit

Permalink
Clarify what counts as 'running'
Browse files Browse the repository at this point in the history
  • Loading branch information
oremanj committed Jul 11, 2023
1 parent 0cfdab8 commit c34f2d1
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 36 deletions.
78 changes: 45 additions & 33 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
<https://trio.readthedocs.io/en/stable/reference-lowlevel.html#using-guest-mode-to-run-trio-on-top-of-other-event-loops>`__
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
<https://trio-asyncio.readthedocs.io/en/latest/>`__ 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

Expand Down
13 changes: 13 additions & 0 deletions newsfragments/39.feature.rst
Original file line number Diff line number Diff line change
@@ -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.
25 changes: 22 additions & 3 deletions sniffio/_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
<https://trio.readthedocs.io/en/stable/reference-lowlevel.html#using-guest-mode-to-run-trio-on-top-of-other-event-loops>`__
and `trio-asyncio <https://trio-asyncio.readthedocs.io/en/latest/>`__
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"``.
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit c34f2d1

Please sign in to comment.