Repository: python-trio/trio

Issue:
trio's guest mode breaks sniffio.current_async_library in certain cases
I am opening this bug as requested on Gitter (see link below).

## Summary
When running trio as a guest (in a `tkinter` event loop in this case), and using `trio.Nursery::start_soon()` from the host event loop to schedule work to be done in the trio event loop, `sniffio.current_async_library()` doesn't detect the trio loop and raises.

## Details
When running trio in guest mode in the `tkinter` event loop, the docs advise to have both loop have the same lifetime. This is achieved by passing a `done_callback` to `trio.start_guest_run()` that terminates the host loop. The closing mechanism is then to terminate the trio loop (most likely by cancelling it), which in turn terminates the host loop.

The doc does not provide examples on how to start async function inside the trio loop from the host loop. When reading it, it was unclear to me what functions or methods are safe to call from *outside* the trio loop. I considered `trio.Nursery::start_soon`, `trio.SendChannel::put_nowait()`, `trio.lowlevel.TrioToken::run_sync_soon()` and `trio.lowlevel.spawn_system_task()`.

As nurseries seemed like an excellent control flow mechanism and a central concept in trio, I tried the Nursery. A Nursery is created in the trio loop, immediately calls `nursery.start_soon(trio.sleep_forever)` to remain available, and the host loop gets a reference to this nursery to schedule async function in it.

This works for trio code, but seem to breaks `sniffio.current_async_library()`, which raises an exception. I my particular use-case, `sniffio` was used under the hood by `asks`, which was used for http requests.

I wrote a (relatively) small example code to showcase the issue, as well as the fix proposed on Gitter. See the link below.

I am unsure if this is an actual bug in sniffio, or trio guest mode, or bad code on my part.
Maybe the doc of `trio.lowlevel.start_guest_run()` could be improved to show how to do this correctly (e.g. using memory channels), so as to avoid future confusion or unsafe code.

## Additional resources
- discussion on gitter https://gitter.im/python-trio/general?at=619f8ac5c6149e53494c05b7
- reproducing code: https://gist.github.com/QuentinSoubeyran/138f0784f9d596f5727ca2bea830ffcd

### Versions:
- OS: Ubuntu 20.04
- python 3.10
- trio 0.19.0
- sniffio 1.2.0

If any info is missing, I'll gladly improve this issue !

## Words of thank
Thank you all for the trio lib, it looks awesome ! I was extremely glade to find about guest mode, it really feels like the *correct* way to use async I/O and a UI together.


Discussion:

Resolving patch:
diff --git a/docs/source/conf.py b/docs/source/conf.py
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -97,6 +97,7 @@ def setup(app):
     "python": ("https://docs.python.org/3", None),
     "outcome": ("https://outcome.readthedocs.io/en/latest/", None),
     "pyopenssl": ("https://www.pyopenssl.org/en/stable/", None),
+    "sniffio": ("https://sniffio.readthedocs.io/en/latest/", None),
 }
 
 autodoc_member_order = "bysource"
diff --git a/trio/_core/_run.py b/trio/_core/_run.py
--- a/trio/_core/_run.py
+++ b/trio/_core/_run.py
@@ -1553,11 +1553,24 @@ def spawn_impl(
         if nursery is None:
             assert self.init_task is None
 
+        ######
+        # Propagate contextvars, and make sure that async_fn can use sniffio.
+        ######
+        if context is None:
+            if system_task:
+                context = self.system_context.copy()
+            else:
+                context = copy_context()
+        # start_soon() or spawn_system_task() might have been invoked
+        # from a different async library; make sure the new task
+        # understands it's Trio-flavored.
+        context.run(current_async_library_cvar.set, "trio")
+
         ######
         # Call the function and get the coroutine object, while giving helpful
         # errors for common mistakes.
         ######
-        coro = coroutine_or_error(async_fn, *args)
+        coro = context.run(coroutine_or_error, async_fn, *args)
 
         if name is None:
             name = async_fn
@@ -1569,12 +1582,6 @@ def spawn_impl(
             except AttributeError:
                 name = repr(name)
 
-        if context is None:
-            if system_task:
-                context = self.system_context.copy()
-            else:
-                context = copy_context()
-
         if not hasattr(coro, "cr_frame"):
             # This async function is implemented in C or Cython
             async def python_wrapper(orig_coro):
@@ -1703,7 +1710,6 @@ def spawn_system_task(self, async_fn, *args, name=None, context=None):
           Task: the newly spawned task
 
         """
-        current_async_library_cvar.set("trio")
         return self.spawn_impl(
             async_fn,
             args,
@@ -1954,7 +1960,6 @@ def setup_runner(
     instruments = Instruments(instruments)
     io_manager = TheIOManager()
     system_context = copy_context()
-    system_context.run(current_async_library_cvar.set, "trio")
     ki_manager = KIManager()
 
     runner = Runner(
diff --git a/trio/_threads.py b/trio/_threads.py
--- a/trio/_threads.py
+++ b/trio/_threads.py
@@ -313,7 +313,6 @@ async def await_in_trio_thread_task():
             )
 
     context = contextvars.copy_context()
-    context.run(current_async_library_cvar.set, "trio")
     return _run_fn_as_system_task(
         callback,
         afn,


Test patch:
diff --git a/trio/_core/tests/test_run.py b/trio/_core/tests/test_run.py
--- a/trio/_core/tests/test_run.py
+++ b/trio/_core/tests/test_run.py
@@ -2006,11 +2006,25 @@ def test_sniffio_integration():
     async def check_inside_trio():
         assert sniffio.current_async_library() == "trio"
 
+    def check_function_returning_coroutine():
+        assert sniffio.current_async_library() == "trio"
+        return check_inside_trio()
+
     _core.run(check_inside_trio)
 
     with pytest.raises(sniffio.AsyncLibraryNotFoundError):
         sniffio.current_async_library()
 
+    async def check_new_task_resets_sniffio_library():
+        sniffio.current_async_library_cvar.set("nullio")
+        _core.spawn_system_task(check_inside_trio)
+        async with _core.open_nursery() as nursery:
+            nursery.start_soon(check_inside_trio)
+            nursery.start_soon(check_function_returning_coroutine)
+        assert sniffio.current_async_library() == "nullio"
+
+    _core.run(check_new_task_resets_sniffio_library)
+
 
 async def test_Task_custom_sleep_data():
     task = _core.current_task()