Skip to content
Draft
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
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ hashbrown = { version = ">= 0.14.5, < 0.16", optional = true }
indexmap = { version = ">= 2.5.0, < 3", optional = true }
num-bigint = { version = "0.4.2", optional = true }
num-complex = { version = ">= 0.4.6, < 0.5", optional = true }
num-rational = {version = "0.4.1", optional = true }
num-rational = { version = "0.4.1", optional = true }
rust_decimal = { version = "1.15", default-features = false, optional = true }
serde = { version = "1.0", optional = true }
smallvec = { version = "1.0", optional = true }
Expand All @@ -63,7 +63,7 @@ rayon = "1.6.1"
futures = "0.3.28"
tempfile = "3.12.0"
static_assertions = "1.1.0"
uuid = {version = "1.10.0", features = ["v4"] }
uuid = { version = "1.10.0", features = ["v4"] }

[build-dependencies]
pyo3-build-config = { path = "pyo3-build-config", version = "=0.23.3", features = ["resolve-config"] }
Expand All @@ -74,6 +74,9 @@ default = ["macros"]
# Enables support for `async fn` for `#[pyfunction]` and `#[pymethods]`.
experimental-async = ["macros", "pyo3-macros/experimental-async"]

# Switch coroutine implementation to anyio instead of asyncio
anyio = ["experimental-async"]

# Enables pyo3::inspect module and additional type information on FromPyObject
# and IntoPy traits
experimental-inspect = []
Expand Down
1 change: 1 addition & 0 deletions guide/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
- [Mapping of Rust types to Python types](conversions/tables.md)
- [Conversion traits](conversions/traits.md)
- [Using `async` and `await`](async-await.md)
- [Awaiting Python awaitables](async-await/awaiting_python_awaitables)
- [Parallelism](parallelism.md)
- [Supporting Free-Threaded Python](free-threading.md)
- [Debugging](debugging.md)
Expand Down
9 changes: 7 additions & 2 deletions guide/src/async-await.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ async fn sleep(seconds: f64, result: Option<PyObject>) -> Option<PyObject> {
# }
```

*Python awaitables instantiated with this method can only be awaited in *asyncio* context. Other Python async runtime may be supported in the future.*

## `Send + 'static` constraint

Resulting future of an `async fn` decorated by `#[pyfunction]` must be `Send + 'static` to be embedded in a Python object.
Expand Down Expand Up @@ -94,6 +92,13 @@ async fn cancellable(#[pyo3(cancel_handle)] mut cancel: CancelHandle) {
# }
```

## *asyncio* vs. *anyio*

By default, Python awaitables instantiated with `async fn` can only be awaited in *asyncio* context.

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.
However, it requires to have the [*sniffio*](https://github.com/python-trio/sniffio) (or *anyio*) library installed.

## The `Coroutine` type

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).
Expand Down
62 changes: 62 additions & 0 deletions guide/src/async-await/awaiting_python_awaitables.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Awaiting Python awaitables

Python awaitable can be awaited on Rust side
using [`await_in_coroutine`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/fn.await_in_coroutine).

```rust
# # ![allow(dead_code)]
# #[cfg(feature = "experimental-async")] {
use pyo3::{prelude::*, coroutine::await_in_coroutine};

#[pyfunction]
async fn wrap_awaitable(awaitable: PyObject) -> PyResult<PyObject> {
Python::with_gil(|gil| await_in_coroutine(awaitable.bind(gil)))?.await
}
# }
```

Behind the scene, `await_in_coroutine` calls the `__await__` method of the Python awaitable (or `__iter__` for
generator-based coroutine).

## Restrictions

As the name suggests, `await_in_coroutine` resulting future can only be awaited in coroutine context. Otherwise, it
panics.

```rust
# # ![allow(dead_code)]
# #[cfg(feature = "experimental-async")] {
use pyo3::{prelude::*, coroutine::await_in_coroutine};

#[pyfunction]
fn block_on(awaitable: PyObject) -> PyResult<PyObject> {
let future = Python::with_gil(|gil| await_in_coroutine(awaitable.bind(gil)))?;
futures::executor::block_on(future) // ERROR: Python awaitable must be awaited in coroutine context
}
# }
```

The future must also be the only one to be awaited at a time; it means that it's forbidden to await it in a `select!`.
Otherwise, it panics.

```rust
# # ![allow(dead_code)]
# #[cfg(feature = "experimental-async")] {
use futures::FutureExt;
use pyo3::{prelude::*, coroutine::await_in_coroutine};

#[pyfunction]
async fn select(awaitable: PyObject) -> PyResult<PyObject> {
let future = Python::with_gil(|gil| await_in_coroutine(awaitable.bind(gil)))?;
futures::select_biased! {
_ = std::future::pending::<()>().fuse() => unreachable!(),
res = future.fuse() => res, // ERROR: Python awaitable mixed with Rust future
}
}
# }
```

These restrictions exist because awaiting a `await_in_coroutine` future strongly binds it to the
enclosing coroutine. The coroutine will then delegate its `send`/`throw`/`close` methods to the
awaited future. If it was awaited in a `select!`, `Coroutine::send` would no able to know if
the value passed would have to be delegated or not.
1 change: 1 addition & 0 deletions guide/src/building-and-distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ There are many ways to go about this: it is possible to use `cargo` to build the
PyO3 has some Cargo features to configure projects for building Python extension modules:
- The `extension-module` feature, which must be enabled when building Python extension modules.
- 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.
- 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.

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.

Expand Down
1 change: 1 addition & 0 deletions newsfragments/3611.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `coroutine::await_in_coroutine` to await awaitables in coroutine context
1 change: 1 addition & 0 deletions newsfragments/3612.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support anyio with a Cargo feature
13 changes: 9 additions & 4 deletions pyo3-ffi/src/abstract_.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::object::*;
use crate::pyport::Py_ssize_t;
use std::os::raw::{c_char, c_int};

#[cfg(any(Py_3_12, all(Py_3_8, not(Py_LIMITED_API))))]
use libc::size_t;
use std::os::raw::{c_char, c_int};

use crate::{object::*, pyport::Py_ssize_t};

#[inline]
#[cfg(all(not(Py_3_13), not(PyPy)))] // CPython exposed as a function in 3.13, in object.h
Expand Down Expand Up @@ -143,7 +144,11 @@ extern "C" {
pub fn PyIter_Next(arg1: *mut PyObject) -> *mut PyObject;
#[cfg(all(not(PyPy), Py_3_10))]
#[cfg_attr(PyPy, link_name = "PyPyIter_Send")]
pub fn PyIter_Send(iter: *mut PyObject, arg: *mut PyObject, presult: *mut *mut PyObject);
pub fn PyIter_Send(
iter: *mut PyObject,
arg: *mut PyObject,
presult: *mut *mut PyObject,
) -> c_int;

#[cfg_attr(PyPy, link_name = "PyPyNumber_Check")]
pub fn PyNumber_Check(o: *mut PyObject) -> c_int;
Expand Down
3 changes: 2 additions & 1 deletion pytests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ publish = false
rust-version = "1.63"

[dependencies]
pyo3 = { path = "../", features = ["extension-module"] }
futures = "0.3.29"
pyo3 = { path = "../", features = ["extension-module", "anyio"] }

[build-dependencies]
pyo3-build-config = { path = "../pyo3-build-config" }
Expand Down
1 change: 1 addition & 0 deletions pytests/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ classifiers = [

[project.optional-dependencies]
dev = [
"anyio[trio]>=4.0",
"hypothesis>=3.55",
"pytest-asyncio>=0.21",
"pytest-benchmark>=3.4",
Expand Down
34 changes: 34 additions & 0 deletions pytests/src/anyio.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use std::{task::Poll, thread, time::Duration};

use futures::{channel::oneshot, future::poll_fn};
use pyo3::prelude::*;

#[pyfunction(signature = (seconds, result = None))]
async fn sleep(seconds: f64, result: Option<PyObject>) -> Option<PyObject> {
if seconds <= 0.0 {
let mut ready = false;
poll_fn(|cx| {
if ready {
return Poll::Ready(());
}
ready = true;
cx.waker().wake_by_ref();
Poll::Pending
})
.await;
} else {
let (tx, rx) = oneshot::channel();
thread::spawn(move || {
thread::sleep(Duration::from_secs_f64(seconds));
tx.send(()).unwrap();
});
rx.await.unwrap();
}
result
}

#[pymodule]
pub fn anyio(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sleep, m)?)?;
Ok(())
}
3 changes: 3 additions & 0 deletions pytests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use pyo3::prelude::*;
use pyo3::types::PyDict;
use pyo3::wrap_pymodule;

pub mod anyio;
pub mod awaitable;
pub mod buf_and_str;
pub mod comparisons;
Expand All @@ -19,6 +20,7 @@ pub mod subclassing;

#[pymodule(gil_used = false)]
fn pyo3_pytests(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_wrapped(wrap_pymodule!(anyio::anyio))?;
m.add_wrapped(wrap_pymodule!(awaitable::awaitable))?;
#[cfg(not(Py_LIMITED_API))]
m.add_wrapped(wrap_pymodule!(buf_and_str::buf_and_str))?;
Expand All @@ -41,6 +43,7 @@ fn pyo3_pytests(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {

let sys = PyModule::import(py, "sys")?;
let sys_modules = sys.getattr("modules")?.downcast_into::<PyDict>()?;
sys_modules.set_item("pyo3_pytests.anyio", m.getattr("anyio")?)?;
sys_modules.set_item("pyo3_pytests.awaitable", m.getattr("awaitable")?)?;
sys_modules.set_item("pyo3_pytests.buf_and_str", m.getattr("buf_and_str")?)?;
sys_modules.set_item("pyo3_pytests.comparisons", m.getattr("comparisons")?)?;
Expand Down
14 changes: 14 additions & 0 deletions pytests/tests/test_anyio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import asyncio

from pyo3_pytests.anyio import sleep
import trio


def test_asyncio():
assert asyncio.run(sleep(0)) is None
assert asyncio.run(sleep(0.1, 42)) == 42


def test_trio():
assert trio.run(sleep, 0) is None
assert trio.run(sleep, 0.1, 42) == 42
Loading
Loading