Skip to content

Commit 8c571fa

Browse files
committed
feat: support anyio with a Cargo feature
Asyncio is the standard and de facto main Python async runtime. Among non-standard runtime, only trio seems to have substantial traction, especially thanks to the anyio project. There is indeed a strong trend for anyio (e.g. FastApi), which can justify a dedicated support.
1 parent 1bf4f4c commit 8c571fa

File tree

13 files changed

+240
-7
lines changed

13 files changed

+240
-7
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ default = ["macros"]
6969
# Enables support for `async fn` for `#[pyfunction]` and `#[pymethods]`.
7070
experimental-async = ["macros", "pyo3-macros/experimental-async"]
7171

72+
# Switch coroutine implementation to anyio instead of asyncio
73+
anyio = ["experimental-async"]
74+
7275
# Enables pyo3::inspect module and additional type information on FromPyObject
7376
# and IntoPy traits
7477
experimental-inspect = []

guide/src/async-await.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ async fn sleep(seconds: f64, result: Option<PyObject>) -> Option<PyObject> {
2424
# }
2525
```
2626

27-
*Python awaitables instantiated with this method can only be awaited in *asyncio* context. Other Python async runtime may be supported in the future.*
28-
2927
## `Send + 'static` constraint
3028

3129
Resulting future of an `async fn` decorated by `#[pyfunction]` must be `Send + 'static` to be embedded in a Python object.
@@ -93,6 +91,13 @@ async fn cancellable(#[pyo3(cancel_handle)] mut cancel: CancelHandle) {
9391
# }
9492
```
9593

94+
## *asyncio* vs. *anyio*
95+
96+
By default, Python awaitables instantiated with `async fn` can only be awaited in *asyncio* context.
97+
98+
PyO3 can also target [*anyio*](https://github.com/agronholm/anyio) with the dedicated `anyio` Cargo feature. With it enabled, `async fn` become awaitable both in *asyncio* or [*trio*](https://github.com/python-trio/trio) context.
99+
However, it requires to have the [*sniffio*](https://github.com/python-trio/sniffio) (or *anyio*) library installed.
100+
96101
## The `Coroutine` type
97102

98103
To make a Rust future awaitable in Python, PyO3 defines a [`Coroutine`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.Coroutine.html) type, which implements the Python [coroutine protocol](https://docs.python.org/3/library/collections.abc.html#collections.abc.Coroutine).

guide/src/building-and-distribution.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ There are many ways to go about this: it is possible to use `cargo` to build the
6262
PyO3 has some Cargo features to configure projects for building Python extension modules:
6363
- The `extension-module` feature, which must be enabled when building Python extension modules.
6464
- The `abi3` feature and its version-specific `abi3-pyXY` companions, which are used to opt-in to the limited Python API in order to support multiple Python versions in a single wheel.
65+
- The `anyio` feature, making PyO3 coroutines target [*anyio*](https://github.com/agronholm/anyio) instead of *asyncio*; either [*sniffio*](https://github.com/python-trio/sniffio) or *anyio* should be added as dependency of the Python extension.
6566

6667
This section describes each of these packaging tools before describing how to build manually without them. It then proceeds with an explanation of the `extension-module` feature. Finally, there is a section describing PyO3's `abi3` features.
6768

newsfragments/3612.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support anyio with a Cargo feature

pytests/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ edition = "2021"
77
publish = false
88

99
[dependencies]
10-
pyo3 = { path = "../", features = ["extension-module"] }
10+
futures = "0.3.29"
11+
pyo3 = { path = "../", features = ["extension-module", "anyio"] }
1112

1213
[build-dependencies]
1314
pyo3-build-config = { path = "../pyo3-build-config" }

pytests/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ classifiers = [
2020

2121
[project.optional-dependencies]
2222
dev = [
23+
"anyio[trio]>=4.0",
2324
"gevent>=22.10.2; implementation_name == 'cpython'",
2425
"hypothesis>=3.55",
2526
"pytest-asyncio>=0.21",

pytests/src/anyio.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use std::{task::Poll, thread, time::Duration};
2+
3+
use futures::{channel::oneshot, future::poll_fn};
4+
use pyo3::prelude::*;
5+
6+
#[pyfunction]
7+
async fn sleep(seconds: f64, result: Option<PyObject>) -> Option<PyObject> {
8+
if seconds <= 0.0 {
9+
let mut ready = false;
10+
poll_fn(|cx| {
11+
if ready {
12+
return Poll::Ready(());
13+
}
14+
ready = true;
15+
cx.waker().wake_by_ref();
16+
Poll::Pending
17+
})
18+
.await;
19+
} else {
20+
let (tx, rx) = oneshot::channel();
21+
thread::spawn(move || {
22+
thread::sleep(Duration::from_secs_f64(seconds));
23+
tx.send(()).unwrap();
24+
});
25+
rx.await.unwrap();
26+
}
27+
result
28+
}
29+
30+
#[pymodule]
31+
pub fn anyio(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
32+
m.add_function(wrap_pyfunction!(sleep, m)?)?;
33+
Ok(())
34+
}

pytests/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use pyo3::prelude::*;
22
use pyo3::types::PyDict;
33
use pyo3::wrap_pymodule;
44

5+
pub mod anyio;
56
pub mod awaitable;
67
pub mod buf_and_str;
78
pub mod comparisons;
@@ -19,6 +20,7 @@ pub mod subclassing;
1920

2021
#[pymodule]
2122
fn pyo3_pytests(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
23+
m.add_wrapped(wrap_pymodule!(anyio::anyio))?;
2224
m.add_wrapped(wrap_pymodule!(awaitable::awaitable))?;
2325
#[cfg(not(Py_LIMITED_API))]
2426
m.add_wrapped(wrap_pymodule!(buf_and_str::buf_and_str))?;
@@ -41,6 +43,7 @@ fn pyo3_pytests(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
4143

4244
let sys = PyModule::import_bound(py, "sys")?;
4345
let sys_modules = sys.getattr("modules")?.downcast_into::<PyDict>()?;
46+
sys_modules.set_item("pyo3_pytests.anyio", m.getattr("anyio")?)?;
4447
sys_modules.set_item("pyo3_pytests.awaitable", m.getattr("awaitable")?)?;
4548
sys_modules.set_item("pyo3_pytests.buf_and_str", m.getattr("buf_and_str")?)?;
4649
sys_modules.set_item("pyo3_pytests.comparisons", m.getattr("comparisons")?)?;

pytests/tests/test_anyio.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import asyncio
2+
3+
from pyo3_pytests.anyio import sleep
4+
import trio
5+
6+
7+
def test_asyncio():
8+
assert asyncio.run(sleep(0)) is None
9+
assert asyncio.run(sleep(0.1, 42)) == 42
10+
11+
12+
def test_trio():
13+
assert trio.run(sleep, 0) is None
14+
assert trio.run(sleep, 0.1, 42) == 42

src/coroutine.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ use crate::{
1919
IntoPy, Py, PyErr, PyObject, PyResult, Python,
2020
};
2121

22+
#[cfg(feature = "anyio")]
23+
mod anyio;
2224
mod asyncio;
2325
mod awaitable;
2426
mod cancel;
27+
#[cfg(feature = "anyio")]
28+
mod trio;
2529
mod waker;
2630

2731
pub use awaitable::await_in_coroutine;

0 commit comments

Comments
 (0)