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 18ad3a7
Show file tree
Hide file tree
Showing 2 changed files with 67 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
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 18ad3a7

Please sign in to comment.