Skip to content

Commit 8de760b

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 01faf63 commit 8de760b

File tree

13 files changed

+219
-7
lines changed

13 files changed

+219
-7
lines changed

Cargo.toml

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

66+
# Switch coroutine implementation to anyio instead of asyncio
67+
anyio = ["macros"]
68+
6669
# Enables pyo3::inspect module and additional type information on FromPyObject
6770
# and IntoPy traits
6871
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/requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
anyio[trio]>=4.0
12
hypothesis>=3.55
23
pytest>=6.0
34
pytest-asyncio>=0.21

pytests/src/anyio.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use futures::channel::oneshot;
2+
use pyo3::prelude::*;
3+
use std::thread;
4+
use std::time::Duration;
5+
6+
#[pyfunction]
7+
async fn sleep(seconds: f64, result: Option<PyObject>) -> Option<PyObject> {
8+
let (tx, rx) = oneshot::channel();
9+
thread::spawn(move || {
10+
thread::sleep(Duration::from_secs_f64(seconds));
11+
tx.send(()).unwrap();
12+
});
13+
rx.await.unwrap();
14+
result
15+
}
16+
17+
#[pymodule]
18+
pub fn anyio(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
19+
m.add_function(wrap_pyfunction!(sleep, m)?)?;
20+
Ok(())
21+
}

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
@@ -18,8 +18,12 @@ use crate::{
1818
IntoPy, Py, PyErr, PyObject, PyResult, Python,
1919
};
2020

21+
#[cfg(feature = "anyio")]
22+
mod anyio;
2123
mod asyncio;
2224
pub(crate) mod cancel;
25+
#[cfg(feature = "anyio")]
26+
mod trio;
2327
pub(crate) mod waker;
2428

2529
use crate::coroutine::cancel::ThrowCallback;

0 commit comments

Comments
 (0)