Skip to content

feat: trio support via sniffio dispatch in existing API#82

Open
kollektiv wants to merge 1 commit into
PyO3:mainfrom
kollektiv:tyen/trio-support
Open

feat: trio support via sniffio dispatch in existing API#82
kollektiv wants to merge 1 commit into
PyO3:mainfrom
kollektiv:tyen/trio-support

Conversation

@kollektiv
Copy link
Copy Markdown

@kollektiv kollektiv commented Apr 24, 2026

Adds trio support to the existing API. tokio::future_into_py, tokio::into_future, and the async_std equivalents now work whether they're called from asyncio or trio. The running async library is detected via sniffio and the right thing happens. Asyncio behavior is unchanged.

import trio

async def main():
    # rust_awaitable comes from tokio::future_into_py, same as today
    result = await rust_awaitable()

trio.run(main)

Why: downstream Python packages built on this crate want to support trio applications without trio-asyncio shims, and this crate is the layer where the asyncio assumption lives. Temporal's Python SDK is a potential candidate.

Approach:

  • TaskLocals gains a RuntimeKind field; TaskLocals::current(py) auto-detects asyncio vs trio.
  • The existing entry points branch on kind. Asyncio takes the existing code path; trio uses a private src/trio.rs module that parks/wakes the trio task via trio.lowlevel.
  • No new Cargo features. trio/sniffio are optional at runtime; if not installed, everything works as before.

New public API: RuntimeKind, TaskLocals::{current, trio, kind, token}. Everything else is private.

Not supported under trio: local_future_into_py, get_current_loop, run/run_until_complete. These are asyncio-specific by nature and return clear errors.

Tests: 24 integration tests in pytests/test_trio.rs covering round-trips, cancellation, panics, contextvars, and streams; CI runs them across the existing matrix.

Detect the running Python async library at call time (via sniffio) and
dispatch inside the existing entry points, so tokio::future_into_py /
tokio::into_future and the async_std equivalents work unchanged when
called from trio. The asyncio code path is byte-for-byte unchanged.

Design:
- RuntimeKind (#[non_exhaustive]) field on the existing TaskLocals;
  TaskLocals::current(py) sniffs the running library. New
  TaskLocals::{trio, kind, token} accessors.
- into_future_with_locals, generic::future_into_py_with_locals, and
  into_stream_with_locals_v1/v2 dispatch on locals.kind() — the asyncio
  arm is the existing body, the trio arm uses src/trio.rs.
- src/trio.rs (private): a RustCoroutine #[pyclass] parks the awaiting
  trio task via trio.lowlevel.wait_task_rescheduled and is woken from
  the Rust executor thread via TrioToken.run_sync_soon. The user's
  future runs on R::spawn; the coroutine awaits a oneshot::Receiver.
- contextvars are propagated into the spawned trio system task via
  spawn_system_task(context=...) (requires trio>=0.23).
- No new Cargo features. trio and sniffio are imported lazily; if
  absent, everything falls through to the asyncio path.
- local_future_into_py returns NotImplementedError under trio;
  get_current_loop returns RuntimeError under trio. run/
  run_until_complete remain asyncio-only by design.

asyncio-side fixes picked up along the way:
- into_stream_v2: a raising async generator now closes the stream
  promptly (previously relied on SenderGlue GC), and the captured
  contextvars.Context is now passed through to the forwarding task.

Testing:
- pytests/test_trio.rs (24 tests) + pytests/test_async_std_trio.rs
  exercise the public tokio::* / async_std::* entry points under
  trio.run: round-trips, sniffio dispatch, panic propagation,
  contextvars, into_future error/BaseException handling, into_stream
  v1/v2 (cfg unstable-streams), the local_future_into_py
  NotImplementedError, and a move_on_after cancellation regression
  test for RustCoroutine.send().
- CI installs trio>=0.23 + sniffio across the matrix (free-threaded
  3.13 on Windows excluded — cffi unsupported there).
@kollektiv kollektiv marked this pull request as ready for review April 24, 2026 17:43
@davidhewitt
Copy link
Copy Markdown
Member

Thanks for the PR. A few comments:

  • This is a fairly large PR with no prior discussion and looks like substantially AI generated. This creates a lot of work for reviewers. Please consider engaging in discussion before dropping such a work item.
  • PyO3 is working on async support in the main crate, I would prefer not to merge modifications to the future creation functions here as I hope they will one day not be needed.
  • Accordingly, we have feat: support anyio with a Cargo feature pyo3#3612 which is a draft to make PyO3's async support compatible with multiple runtimes.

I would prefer to move the main PyO3 support forward first, and then reconsider what APIs make sense to evolve in this crate after.

Help on doing so is welcome (but please consider carefully crafted discussion and PRs which are individually reviewable, not AI-generated monoliths).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants