Skip to content

Commit e1d5f5c

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 53cccb4 commit e1d5f5c

File tree

14 files changed

+249
-13
lines changed

14 files changed

+249
-13
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ pyo3-build-config = { path = "pyo3-build-config", version = "0.21.0-dev", featur
6464
[features]
6565
default = ["macros"]
6666

67+
# Switch coroutine implementation to anyio instead of asyncio
68+
anyio = ["macros"]
69+
6770
# Enables pyo3::inspect module and additional type information on FromPyObject
6871
# and IntoPy traits
6972
experimental-inspect = []

guide/src/async-await.md

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

25-
*Python awaitables instantiated with this method can only be awaited in *asyncio* context. Other Python async runtime may be supported in the future.*
26-
2725
## `Send + 'static` constraint
2826

2927
Resulting future of an `async fn` decorated by `#[pyfunction]` must be `Send + 'static` to be embedded in a Python object.
@@ -85,6 +83,13 @@ async fn cancellable(#[pyo3(cancel_handle)] mut cancel: CancelHandle) {
8583
}
8684
```
8785

86+
## *asyncio* vs. *anyio*
87+
88+
By default, Python awaitables instantiated with `async fn` can only be awaited in *asyncio* context.
89+
90+
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.
91+
However, it requires to have the [*sniffio*](https://github.com/python-trio/sniffio) (or *anyio*) library installed.
92+
8893
## The `Coroutine` type
8994

9095
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
"hypothesis>=3.55",
2425
"pytest-asyncio>=0.21",
2526
"pytest-benchmark>=3.4",

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::{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;
@@ -18,6 +19,7 @@ pub mod subclassing;
1819

1920
#[pymodule]
2021
fn pyo3_pytests(py: Python<'_>, m: &PyModule) -> PyResult<()> {
22+
m.add_wrapped(wrap_pymodule!(anyio::anyio))?;
2123
m.add_wrapped(wrap_pymodule!(awaitable::awaitable))?;
2224
#[cfg(not(Py_LIMITED_API))]
2325
m.add_wrapped(wrap_pymodule!(buf_and_str::buf_and_str))?;
@@ -39,6 +41,7 @@ fn pyo3_pytests(py: Python<'_>, m: &PyModule) -> PyResult<()> {
3941

4042
let sys = PyModule::import(py, "sys")?;
4143
let sys_modules: &PyDict = sys.getattr("modules")?.downcast()?;
44+
sys_modules.set_item("pyo3_pytests.anyio", m.getattr("anyio")?)?;
4245
sys_modules.set_item("pyo3_pytests.awaitable", m.getattr("awaitable")?)?;
4346
sys_modules.set_item("pyo3_pytests.buf_and_str", m.getattr("buf_and_str")?)?;
4447
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,8 +19,12 @@ use crate::{
1919
IntoPy, Py, PyErr, PyObject, PyResult, Python,
2020
};
2121

22+
#[cfg(feature = "anyio")]
23+
mod anyio;
2224
mod asyncio;
2325
pub(crate) mod cancel;
26+
#[cfg(feature = "anyio")]
27+
mod trio;
2428
pub(crate) mod waker;
2529

2630
pub use cancel::CancelHandle;

0 commit comments

Comments
 (0)