Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,17 @@ jobs:
- name: Build
run: cargo build --features=${{env.features}} --verbose --target ${{ matrix.platform.rust-target }}

# trio on Windows depends on cffi, which doesn't support
# free-threaded CPython 3.13 (only 3.14t+). PyPy is excluded
# because tests are skipped on PyPy anyway.
# trio>=0.23 for spawn_system_task(context=...).
- if: ${{ !startsWith(matrix.python-version, 'pypy') && !(matrix.platform.os == 'windows-latest' && matrix.python-version == '3.13t') }}
name: Install trio and sniffio
run: python -m pip install -U 'trio>=0.23' sniffio
- if: ${{ matrix.platform.os == 'windows-latest' && matrix.python-version == '3.13t' }}
name: Skip trio (cffi unsupported on free-threaded 3.13)
run: echo "PYO3_ASYNC_TEST_TRIO_OPTIONAL=1" >> $env:GITHUB_ENV

# uvloop doesn't compile under
# Windows, https://github.com/MagicStack/uvloop/issues/536,
# nor PyPy, https://github.com/MagicStack/uvloop/issues/537
Expand Down Expand Up @@ -136,7 +147,7 @@ jobs:
- uses: taiki-e/install-action@cargo-llvm-cov

- name: Install pyo3-asyncio test dependencies
run: python -m pip install -U uvloop
run: python -m pip install -U uvloop 'trio>=0.23' sniffio

- run: cargo llvm-cov --all-features --codecov --output-path coverage.json
- uses: codecov/codecov-action@v5
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ To see unreleased changes, please see the CHANGELOG on the main branch.

## [Unreleased]

- Add trio support: `TaskLocals`, `into_future_with_locals`, `generic::future_into_py_with_locals`, `into_stream_with_locals_v1` and `into_stream_with_locals_v2` now detect the running Python async library via `sniffio` and dispatch accordingly, so the existing `tokio::future_into_py`/`tokio::into_future` (and `async_std` equivalents) work unchanged when called from `trio`. No new public API or feature flag is required; the asyncio code path is unchanged. New `RuntimeKind` enum and `TaskLocals::{trio, current, kind, token}` are exposed for explicit control. `local_future_into_py_with_locals` returns `NotImplementedError` under trio (`spawn_local` requires a `LocalSet` incompatible with `trio.run`); `run`/`run_until_complete` remain asyncio-only. Requires `trio >= 0.23`.
- `into_stream_v2`: a raising async generator now closes the stream promptly under asyncio (previously relied on `SenderGlue` GC), and the captured `TaskLocals` `contextvars.Context` is now propagated into the forwarding task under both asyncio and trio.

## [0.28.0] - 2026-02-03

- Bump to pyo3 0.28. [#76](https://github.com/PyO3/pyo3-async-runtimes/pull/76)
Expand Down
12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,18 @@ harness = false
required-features = ["tokio-runtime", "testing"]


[[test]]
name = "test_trio"
path = "pytests/test_trio.rs"
harness = false
required-features = ["tokio-runtime"]

[[test]]
name = "test_async_std_trio"
path = "pytests/test_async_std_trio.rs"
harness = false
required-features = ["async-std-runtime"]

[[test]]
name = "test_race_condition_regression"
path = "pytests/test_race_condition_regression.rs"
Expand Down
12 changes: 11 additions & 1 deletion Contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,14 @@ Using the project's githooks are recommended to prevent CI from failing for triv

```
git config core.hookspath .githooks
```
```

## Running the trio tests

The trio integration tests need the `trio` and `sniffio` Python packages installed
(`pip install trio sniffio`). Add `unstable-streams` for the stream-conversion tests:

```
cargo test --features tokio-runtime --test test_trio
cargo test --features 'tokio-runtime unstable-streams' --test test_trio
```
64 changes: 60 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

***Forked from [`pyo3-asyncio`](https://github.com/awestlake87/pyo3-asyncio/) to deliver compatibility for PyO3 0.21+.***

[Rust](http://www.rust-lang.org/) bindings for [Python](https://www.python.org/)'s [Asyncio Library](https://docs.python.org/3/library/asyncio.html). This crate facilitates interactions between Rust Futures and Python Coroutines and manages the lifecycle of their corresponding event loops.
[Rust](http://www.rust-lang.org/) bindings for [Python](https://www.python.org/)'s [Asyncio Library](https://docs.python.org/3/library/asyncio.html) and [trio](https://trio.readthedocs.io/). This crate facilitates interactions between Rust Futures and Python Coroutines and manages the lifecycle of their corresponding event loops.

- PyO3 Project: [Homepage](https://pyo3.rs/) | [GitHub](https://github.com/PyO3/pyo3)

Expand All @@ -30,9 +30,10 @@ If you are working with a Python library that makes use of async functions or wi
Python bindings for an async Rust library, [`pyo3-async-runtimes`](https://github.com/PyO3/pyo3-async-runtimes)
likely has the tools you need. It provides conversions between async functions in both Python and
Rust and was designed with first-class support for popular Rust runtimes such as
[`tokio`](https://tokio.rs/) and [`async-std`](https://async.rs/). In addition, all async Python
code runs on the default `asyncio` event loop, so `pyo3-async-runtimes` should work just fine with existing
Python libraries.
[`tokio`](https://tokio.rs/) and [`async-std`](https://async.rs/). By default, async Python
code runs on the `asyncio` event loop, so `pyo3-async-runtimes` should work just fine with existing
Python libraries. The same conversions also work transparently under [`trio`](https://trio.readthedocs.io)
— the running Python async library is detected at call time via `sniffio`, with no extra feature flags.

In the following sections, we'll give a general overview of `pyo3-async-runtimes` explaining how to call
async Python functions with PyO3, how to call async Rust functions from Python, and how to configure
Expand Down Expand Up @@ -529,6 +530,61 @@ fn main() -> PyResult<()> {
}
```

#### Using `trio`

Unlike `uvloop`, [`trio`](https://trio.readthedocs.io) is not a drop-in
`asyncio` event loop — it is a separate async library with its own
scheduler and primitives. `pyo3-async-runtimes` detects the running
Python async library at call time (via
[`sniffio`](https://sniffio.readthedocs.io)) and uses the appropriate
park/wake primitives, so the same compiled extension works under both
`asyncio` and `trio` with no Python-side shim and no extra Cargo
features:

```rust
//! lib.rs

use pyo3::{prelude::*, wrap_pyfunction};

#[pyfunction]
fn rust_sleep(py: Python) -> PyResult<Bound<PyAny>> {
pyo3_async_runtimes::tokio::future_into_py(py, async {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Ok(())
})
}

#[pymodule]
fn my_async_module(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(rust_sleep, m)?)?;
Ok(())
}
```

```python
import trio
from my_async_module import rust_sleep

async def main():
await rust_sleep()

trio.run(main)
```

The same `rust_sleep` can be awaited unchanged from `asyncio.run(main())`.
Under `asyncio` the existing code path is taken and an `asyncio.Future`
is returned exactly as before, so existing users see no behavior change.

`into_future`, `future_into_py`, and `into_stream_v2` all dispatch this
way. `local_future_into_py` (the `!Send` variant) and the
`run`/`run_until_complete` helpers remain asyncio-only —
`local_future_into_py` returns `NotImplementedError` under trio because
`spawn_local` requires a `LocalSet` that cannot share a thread with
`trio.run`, and `run_until_complete` is inherently tied to asyncio's
loop-creation API.

Requires `trio >= 0.23`.

### Additional Information

- Managing event loop references can be tricky with `pyo3-async-runtimes`. See [Event Loop References and ContextVars](https://docs.rs/pyo3-async-runtimes/latest/pyo3_async_runtimes/#event-loop-references-and-contextvars) in the API docs to get a better intuition for how event loop references are managed in this library.
Expand Down
36 changes: 36 additions & 0 deletions pytests/test_async_std_trio.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use pyo3::prelude::*;

#[pyfunction]
fn rust_sleep(py: Python<'_>) -> PyResult<Bound<'_, PyAny>> {
pyo3_async_runtimes::async_std::future_into_py(py, async move {
async_std::task::sleep(std::time::Duration::from_millis(50)).await;
Ok(42i64)
})
}

fn main() -> PyResult<()> {
Python::initialize();
Python::attach(|py| {
if py.import("trio").is_err() {
if std::env::var_os("CI").is_some()
&& std::env::var_os("PYO3_ASYNC_TEST_TRIO_OPTIONAL").is_none()
{
eprintln!("error: trio is not installed but CI is set");
std::process::exit(1);
}
println!("test test_async_std_trio ... skipped (trio not available)");
return Ok(());
}
let driver = PyModule::from_code(
py,
c"import trio\nasync def main(f):\n return await f()\ndef drive(f):\n return trio.run(main, f)\n",
c"trio_driver.py",
c"trio_driver",
)?;
let f = wrap_pyfunction!(rust_sleep, py)?;
let r: i64 = driver.getattr("drive")?.call1((f,))?.extract()?;
assert_eq!(r, 42);
println!("test test_async_std_trio ... ok");
Ok(())
})
}
Loading