Updated async-await guide for pyo3-asyncio 0.14
This commit is contained in:
parent
9a735698df
commit
5c5a406ce4
|
@ -12,6 +12,176 @@ In the following sections, we'll give a general overview of `pyo3-asyncio` expla
|
||||||
async Python functions with PyO3, how to call async Rust functions from Python, and how to configure
|
async Python functions with PyO3, how to call async Rust functions from Python, and how to configure
|
||||||
your codebase to manage the runtimes of both.
|
your codebase to manage the runtimes of both.
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
Here are some examples to get you started right away! A more detailed breakdown
|
||||||
|
of the concepts in these examples can be found in the following sections.
|
||||||
|
|
||||||
|
### Rust Applications
|
||||||
|
Here we initialize the runtime, import Python's `asyncio` library and run the given future to completion using Python's default `EventLoop` and `async-std`. Inside the future, we convert `asyncio` sleep into a Rust future and await it.
|
||||||
|
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Cargo.toml dependencies
|
||||||
|
[dependencies]
|
||||||
|
pyo3 = { version = "0.14" }
|
||||||
|
pyo3-asyncio = { version = "0.14", features = ["attributes", "async-std-runtime"] }
|
||||||
|
async-std = "1.9"
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
//! main.rs
|
||||||
|
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
#[pyo3_asyncio::async_std::main]
|
||||||
|
async fn main() -> PyResult<()> {
|
||||||
|
let fut = Python::with_gil(|py| {
|
||||||
|
let asyncio = py.import("asyncio")?;
|
||||||
|
// convert asyncio.sleep into a Rust Future
|
||||||
|
pyo3_asyncio::async_std::into_future(asyncio.call_method1("sleep", (1.into_py(py),))?)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
fut.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The same application can be written to use `tokio` instead using the `#[pyo3_asyncio::tokio::main]`
|
||||||
|
attribute.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Cargo.toml dependencies
|
||||||
|
[dependencies]
|
||||||
|
pyo3 = { version = "0.14" }
|
||||||
|
pyo3-asyncio = { version = "0.14", features = ["attributes", "tokio-runtime"] }
|
||||||
|
tokio = "1.4"
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
//! main.rs
|
||||||
|
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
#[pyo3_asyncio::tokio::main]
|
||||||
|
async fn main() -> PyResult<()> {
|
||||||
|
let fut = Python::with_gil(|py| {
|
||||||
|
let asyncio = py.import("asyncio")?;
|
||||||
|
// convert asyncio.sleep into a Rust Future
|
||||||
|
pyo3_asyncio::tokio::into_future(asyncio.call_method1("sleep", (1.into_py(py),))?)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
fut.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
More details on the usage of this library can be found in the [API docs](https://awestlake87.github.io/pyo3-asyncio/master/doc) and the primer below.
|
||||||
|
|
||||||
|
### PyO3 Native Rust Modules
|
||||||
|
|
||||||
|
PyO3 Asyncio can also be used to write native modules with async functions.
|
||||||
|
|
||||||
|
Add the `[lib]` section to `Cargo.toml` to make your library a `cdylib` that Python can import.
|
||||||
|
```toml
|
||||||
|
[lib]
|
||||||
|
name = "my_async_module"
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Make your project depend on `pyo3` with the `extension-module` feature enabled and select your
|
||||||
|
`pyo3-asyncio` runtime:
|
||||||
|
|
||||||
|
For `async-std`:
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
pyo3 = { version = "0.13", features = ["extension-module"] }
|
||||||
|
pyo3-asyncio = { version = "0.14", features = ["async-std-runtime"] }
|
||||||
|
async-std = "1.9"
|
||||||
|
```
|
||||||
|
|
||||||
|
For `tokio`:
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
pyo3 = { version = "0.13", features = ["extension-module"] }
|
||||||
|
pyo3-asyncio = { version = "0.14", features = ["tokio-runtime"] }
|
||||||
|
tokio = "1.4"
|
||||||
|
```
|
||||||
|
|
||||||
|
Export an async function that makes use of `async-std`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
//! lib.rs
|
||||||
|
|
||||||
|
use pyo3::{prelude::*, wrap_pyfunction};
|
||||||
|
|
||||||
|
#[pyfunction]
|
||||||
|
fn rust_sleep(py: Python) -> PyResult<&PyAny> {
|
||||||
|
pyo3_asyncio::async_std::future_into_py(py, async {
|
||||||
|
async_std::task::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
Ok(Python::with_gil(|py| py.None()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymodule]
|
||||||
|
fn my_async_module(py: Python, m: &PyModule) -> PyResult<()> {
|
||||||
|
m.add_function(wrap_pyfunction!(rust_sleep, m)?)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to use `tokio` instead, here's what your module should look like:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
//! lib.rs
|
||||||
|
|
||||||
|
use pyo3::{prelude::*, wrap_pyfunction};
|
||||||
|
|
||||||
|
#[pyfunction]
|
||||||
|
fn rust_sleep(py: Python) -> PyResult<&PyAny> {
|
||||||
|
pyo3_asyncio::tokio::future_into_py(py, async {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
Ok(Python::with_gil(|py| py.None()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymodule]
|
||||||
|
fn my_async_module(py: Python, m: &PyModule) -> PyResult<()> {
|
||||||
|
m.add_function(wrap_pyfunction!(rust_sleep, m)?)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Build your module and rename `libmy_async_module.so` to `my_async_module.so`
|
||||||
|
```bash
|
||||||
|
cargo build --release && mv target/release/libmy_async_module.so target/release/my_async_module.so
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, point your `PYTHONPATH` to the directory containing `my_async_module.so`, then you'll be able
|
||||||
|
to import and use it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ PYTHONPATH=target/release python3
|
||||||
|
Python 3.8.5 (default, Jan 27 2021, 15:41:15)
|
||||||
|
[GCC 9.3.0] on linux
|
||||||
|
Type "help", "copyright", "credits" or "license" for more information.
|
||||||
|
>>> import asyncio
|
||||||
|
>>>
|
||||||
|
>>> from my_async_module import rust_sleep
|
||||||
|
>>>
|
||||||
|
>>> async def main():
|
||||||
|
>>> await rust_sleep()
|
||||||
|
>>>
|
||||||
|
>>> # should sleep for 1s
|
||||||
|
>>> asyncio.run(main())
|
||||||
|
>>>
|
||||||
|
```
|
||||||
|
|
||||||
## Awaiting an Async Python Function in Rust
|
## Awaiting an Async Python Function in Rust
|
||||||
|
|
||||||
Let's take a look at a dead simple async Python function:
|
Let's take a look at a dead simple async Python function:
|
||||||
|
@ -34,20 +204,29 @@ That's where `pyo3-asyncio` comes in.
|
||||||
performs this conversion for us:
|
performs this conversion for us:
|
||||||
|
|
||||||
|
|
||||||
```rust
|
```rust no_run
|
||||||
let future = Python::with_gil(|py| {
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
#[pyo3_asyncio::tokio::main]
|
||||||
|
async fn main() -> PyResult<()> {
|
||||||
|
let future = Python::with_gil(|py| -> PyResult<_> {
|
||||||
// import the module containing the py_sleep function
|
// import the module containing the py_sleep function
|
||||||
let example = py.import("example")?;
|
let example = py.import("example")?;
|
||||||
|
|
||||||
// calling the py_sleep method like a normal function returns a coroutine
|
// calling the py_sleep method like a normal function
|
||||||
|
// returns a coroutine
|
||||||
let coroutine = example.call_method0("py_sleep")?;
|
let coroutine = example.call_method0("py_sleep")?;
|
||||||
|
|
||||||
// convert the coroutine into a Rust future
|
// convert the coroutine into a Rust future using the
|
||||||
pyo3_asyncio::into_future(coroutine)
|
// tokio runtime
|
||||||
})?;
|
pyo3_asyncio::tokio::into_future(coroutine)
|
||||||
|
})?;
|
||||||
|
|
||||||
// await the future
|
// await the future
|
||||||
future.await;
|
future.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> If you're interested in learning more about `coroutines` and `awaitables` in general, check out the
|
> If you're interested in learning more about `coroutines` and `awaitables` in general, check out the
|
||||||
|
@ -61,27 +240,33 @@ Here we have the same async function as before written in Rust using the
|
||||||
```rust
|
```rust
|
||||||
/// Sleep for 1 second
|
/// Sleep for 1 second
|
||||||
async fn rust_sleep() {
|
async fn rust_sleep() {
|
||||||
async_std::task::sleep(Duration::from_secs(1)).await;
|
async_std::task::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Similar to Python, Rust's async functions also return a special object called a
|
Similar to Python, Rust's async functions also return a special object called a
|
||||||
`Future`:
|
`Future`:
|
||||||
|
|
||||||
```rust
|
```rust compile_fail
|
||||||
let future = rust_sleep();
|
let future = rust_sleep();
|
||||||
```
|
```
|
||||||
|
|
||||||
We can convert this `Future` object into Python to make it `awaitable`. This tells Python that you
|
We can convert this `Future` object into Python to make it `awaitable`. This tells Python that you
|
||||||
can use the `await` keyword with it. In order to do this, we'll call
|
can use the `await` keyword with it. In order to do this, we'll call
|
||||||
[`pyo3_asyncio::async_std::into_coroutine`](https://docs.rs/pyo3-asyncio/latest/pyo3_asyncio/async_std/fn.into_coroutine.html):
|
[`pyo3_asyncio::async_std::future_into_py`](https://docs.rs/pyo3-asyncio/latest/pyo3_asyncio/async_std/fn.future_into_py.html):
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
async fn rust_sleep() {
|
||||||
|
async_std::task::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
fn call_rust_sleep(py: Python) -> PyResult<PyObject> {
|
fn call_rust_sleep(py: Python) -> PyResult<&PyAny> {
|
||||||
pyo3_asyncio::async_std::into_coroutine(py, async move {
|
pyo3_asyncio::async_std::future_into_py(py, async move {
|
||||||
rust_sleep().await;
|
rust_sleep().await;
|
||||||
Ok(())
|
Ok(Python::with_gil(|py| py.None()))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -116,7 +301,7 @@ Because we have to block on one of those functions, we can't use [`#[async_std::
|
||||||
since it's not a good idea to make long blocking calls during an async function.
|
since it's not a good idea to make long blocking calls during an async function.
|
||||||
|
|
||||||
> Internally, these `#[main]` proc macros are expanded to something like this:
|
> Internally, these `#[main]` proc macros are expanded to something like this:
|
||||||
> ```rust
|
> ```rust compile_fail
|
||||||
> fn main() {
|
> fn main() {
|
||||||
> // your async main fn
|
> // your async main fn
|
||||||
> async fn _main_impl() { /* ... */ }
|
> async fn _main_impl() { /* ... */ }
|
||||||
|
@ -134,18 +319,20 @@ initialization. These macros are intended to mirror the initialization of `async
|
||||||
while also satisfying the Python runtime's needs.
|
while also satisfying the Python runtime's needs.
|
||||||
|
|
||||||
Here's a full example of PyO3 initialization with the `async-std` runtime:
|
Here's a full example of PyO3 initialization with the `async-std` runtime:
|
||||||
```rust
|
```rust no_run
|
||||||
use pyo3::prelude::*;
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
#[pyo3_asyncio::async_std::main]
|
#[pyo3_asyncio::async_std::main]
|
||||||
async fn main() -> PyResult<()> {
|
async fn main() -> PyResult<()> {
|
||||||
// PyO3 is initialized - Ready to go
|
// PyO3 is initialized - Ready to go
|
||||||
|
|
||||||
let fut = Python::with_gil(|py| {
|
let fut = Python::with_gil(|py| -> PyResult<_> {
|
||||||
let asyncio = py.import("asyncio")?;
|
let asyncio = py.import("asyncio")?;
|
||||||
|
|
||||||
// convert asyncio.sleep into a Rust Future
|
// convert asyncio.sleep into a Rust Future
|
||||||
pyo3_asyncio::into_future(asyncio.call_method1("sleep", (1.into_py(py),))?)
|
pyo3_asyncio::async_std::into_future(
|
||||||
|
asyncio.call_method1("sleep", (1.into_py(py),))?
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
fut.await?;
|
fut.await?;
|
||||||
|
@ -154,6 +341,360 @@ async fn main() -> PyResult<()> {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### A Note About `asyncio.run`
|
||||||
|
|
||||||
|
In Python 3.7+, the recommended way to run a top-level coroutine with `asyncio`
|
||||||
|
is with `asyncio.run`. In `v0.13` we recommended against using this function due to initialization issues, but in `v0.14` it's perfectly valid to use this function... with a caveat.
|
||||||
|
|
||||||
|
Since our Rust <--> Python conversions require a reference to the Python event loop, this poses a problem. Imagine we have a PyO3 Asyncio module that defines
|
||||||
|
a `rust_sleep` function like in previous examples. You might rightfully assume that you can call pass this directly into `asyncio.run` like this:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from my_async_module import rust_sleep
|
||||||
|
|
||||||
|
asyncio.run(rust_sleep())
|
||||||
|
```
|
||||||
|
|
||||||
|
You might be surprised to find out that this throws an error:
|
||||||
|
```bash
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "<stdin>", line 1, in <module>
|
||||||
|
RuntimeError: no running event loop
|
||||||
|
```
|
||||||
|
|
||||||
|
What's happening here is that we are calling `rust_sleep` _before_ the future is
|
||||||
|
actually running on the event loop created by `asyncio.run`. This is counter-intuitive, but expected behaviour, and unfortunately there doesn't seem to be a good way of solving this problem within PyO3 Asyncio itself.
|
||||||
|
|
||||||
|
However, we can make this example work with a simple workaround:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from my_async_module import rust_sleep
|
||||||
|
|
||||||
|
# Calling main will just construct the coroutine that later calls rust_sleep.
|
||||||
|
# - This ensures that rust_sleep will be called when the event loop is running,
|
||||||
|
# not before.
|
||||||
|
async def main():
|
||||||
|
await rust_sleep()
|
||||||
|
|
||||||
|
# Run the main() coroutine at the top-level instead
|
||||||
|
asyncio.run(main())
|
||||||
|
```
|
||||||
|
|
||||||
|
### Non-standard Python Event Loops
|
||||||
|
|
||||||
|
Python allows you to use alternatives to the default `asyncio` event loop. One
|
||||||
|
popular alternative is `uvloop`. In `v0.13` using non-standard event loops was
|
||||||
|
a bit of an ordeal, but in `v0.14` it's trivial.
|
||||||
|
|
||||||
|
#### Using `uvloop` in a PyO3 Asyncio Native Extensions
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Cargo.toml
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "my_async_module"
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
pyo3 = { version = "0.14", features = ["extension-module", "auto-initialize"] }
|
||||||
|
pyo3-asyncio = { version = "0.14", features = ["tokio-runtime"] }
|
||||||
|
async-std = "1.9"
|
||||||
|
tokio = "1.4"
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
//! lib.rs
|
||||||
|
|
||||||
|
use pyo3::{prelude::*, wrap_pyfunction};
|
||||||
|
|
||||||
|
#[pyfunction]
|
||||||
|
fn rust_sleep(py: Python) -> PyResult<&PyAny> {
|
||||||
|
pyo3_asyncio::tokio::future_into_py(py, async {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
Ok(Python::with_gil(|py| py.None()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymodule]
|
||||||
|
fn my_async_module(_py: Python, m: &PyModule) -> PyResult<()> {
|
||||||
|
m.add_function(wrap_pyfunction!(rust_sleep, m)?)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cargo build --release && mv target/release/libmy_async_module.so my_async_module.so
|
||||||
|
Compiling pyo3-asyncio-lib v0.1.0 (pyo3-asyncio-lib)
|
||||||
|
Finished release [optimized] target(s) in 1.00s
|
||||||
|
$ PYTHONPATH=target/release/ python3
|
||||||
|
Python 3.8.8 (default, Apr 13 2021, 19:58:26)
|
||||||
|
[GCC 7.3.0] :: Anaconda, Inc. on linux
|
||||||
|
Type "help", "copyright", "credits" or "license" for more information.
|
||||||
|
>>> import asyncio
|
||||||
|
>>> import uvloop
|
||||||
|
>>>
|
||||||
|
>>> import my_async_module
|
||||||
|
>>>
|
||||||
|
>>> uvloop.install()
|
||||||
|
>>>
|
||||||
|
>>> async def main():
|
||||||
|
... await my_async_module.rust_sleep()
|
||||||
|
...
|
||||||
|
>>> asyncio.run(main())
|
||||||
|
>>>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using `uvloop` in Rust Applications
|
||||||
|
|
||||||
|
Using `uvloop` in Rust applications is a bit trickier, but it's still possible
|
||||||
|
with relatively few modifications.
|
||||||
|
|
||||||
|
> Unfortunately, we can't make use of the `#[pyo3_asyncio::<runtime>::main]` attribute with non-standard event loops. This is because the `#[pyo3_asyncio::<runtime>::main]` proc macro has to interact with the Python
|
||||||
|
event loop before we can install the `uvloop` policy.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
async-std = "1.9"
|
||||||
|
pyo3 = "0.14"
|
||||||
|
pyo3-asyncio = { version = "0.14", features = ["async-std-runtime"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
//! main.rs
|
||||||
|
|
||||||
|
use pyo3::{prelude::*, types::PyType};
|
||||||
|
|
||||||
|
fn main() -> PyResult<()> {
|
||||||
|
pyo3::prepare_freethreaded_python();
|
||||||
|
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
let uvloop = py.import("uvloop")?;
|
||||||
|
uvloop.call_method0("install")?;
|
||||||
|
|
||||||
|
// store a reference for the assertion
|
||||||
|
let uvloop = PyObject::from(uvloop);
|
||||||
|
|
||||||
|
pyo3_asyncio::async_std::run(py, async move {
|
||||||
|
// verify that we are on a uvloop.Loop
|
||||||
|
Python::with_gil(|py| -> PyResult<()> {
|
||||||
|
assert!(uvloop
|
||||||
|
.as_ref(py)
|
||||||
|
.getattr("Loop")?
|
||||||
|
.downcast::<PyType>()
|
||||||
|
.unwrap()
|
||||||
|
.is_instance(pyo3_asyncio::async_std::get_current_loop(py)?)?);
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
async_std::task::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Loop References and Thread-awareness
|
||||||
|
|
||||||
|
One problem that arises when interacting with Python's asyncio library is that the functions we use to get a reference to the Python event loop can only be called in certain contexts. Since PyO3 Asyncio needs to interact with Python's event loop during conversions, the context of these conversions can matter a lot.
|
||||||
|
|
||||||
|
> The core conversions we've mentioned so far in this guide should insulate you from these concerns in most cases, but in the event that they don't, this section should provide you with the information you need to solve these problems.
|
||||||
|
|
||||||
|
#### The Main Dilemma
|
||||||
|
|
||||||
|
Python programs can have many independent event loop instances throughout the lifetime of the application (`asyncio.run` for example creates its own event loop each time it's called for instance), and they can even run concurrent with other event loops. For this reason, the most correct method of obtaining a reference to the Python event loop is via `asyncio.get_running_loop`.
|
||||||
|
|
||||||
|
`asyncio.get_running_loop` returns the event loop associated with the current OS thread. It can be used inside Python coroutines to spawn concurrent tasks, interact with timers, or in our case signal between Rust and Python. This is all well and good when we are operating on a Python thread, but since Rust threads are not associated with a Python event loop, `asyncio.get_running_loop` will fail when called on a Rust runtime.
|
||||||
|
|
||||||
|
#### The Solution
|
||||||
|
|
||||||
|
A really straightforward way of dealing with this problem is to pass a reference to the associated Python event loop for every conversion. That's why in `v0.14`, we introduced a new set of conversion functions that do just that:
|
||||||
|
|
||||||
|
- `pyo3_asyncio::into_future_with_loop` - Convert a Python awaitable into a Rust future with the given asyncio event loop.
|
||||||
|
- `pyo3_asyncio::<runtime>::future_into_py_with_loop` - Convert a Rust future into a Python awaitable with the given asyncio event loop.
|
||||||
|
- `pyo3_asyncio::<runtime>::local_future_into_py_with_loop` - Convert a `!Send` Rust future into a Python awaitable with the given asyncio event loop.
|
||||||
|
|
||||||
|
One clear disadvantage to this approach (aside from the verbose naming) is that the Rust application has to explicitly track its references to the Python event loop. In native libraries, we can't make any assumptions about the underlying event loop, so the only reliable way to make sure our conversions work properly is to store a reference to the current event loop at the callsite to use later on.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
#[pyfunction]
|
||||||
|
fn sleep(py: Python) -> PyResult<&PyAny> {
|
||||||
|
let current_loop = pyo3_asyncio::get_running_loop(py)?;
|
||||||
|
let loop_ref = PyObject::from(current_loop);
|
||||||
|
|
||||||
|
// Convert the async move { } block to a Python awaitable
|
||||||
|
pyo3_asyncio::tokio::future_into_py_with_loop(current_loop, async move {
|
||||||
|
let py_sleep = Python::with_gil(|py| {
|
||||||
|
// Sometimes we need to call other async Python functions within
|
||||||
|
// this future. In order for this to work, we need to track the
|
||||||
|
// event loop from earlier.
|
||||||
|
pyo3_asyncio::into_future_with_loop(
|
||||||
|
loop_ref.as_ref(py),
|
||||||
|
py.import("asyncio")?.call_method1("sleep", (1,))?
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
py_sleep.await?;
|
||||||
|
|
||||||
|
Ok(Python::with_gil(|py| py.None()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymodule]
|
||||||
|
fn my_mod(py: Python, m: &PyModule) -> PyResult<()> {
|
||||||
|
m.add_function(wrap_pyfunction!(sleep, m)?)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> A naive solution to this tracking problem would be to cache a global reference to the asyncio event loop that all PyO3 Asyncio conversions can use. In fact this is what we did in PyO3 Asyncio `v0.13`. This works well for applications, but it soon became clear that this is not so ideal for libraries. Libraries usually have no direct control over how the event loop is managed, they're just expected to work with any event loop at any point in the application. This problem is compounded further when multiple event loops are used in the application since the global reference will only point to one.
|
||||||
|
|
||||||
|
Another disadvantage to this explicit approach that is less obvious is that we can no longer call our `#[pyfunction] fn sleep` on a Rust runtime since `asyncio.get_running_loop` only works on Python threads! It's clear that we need a slightly more flexible approach.
|
||||||
|
|
||||||
|
In order to detect the Python event loop at the callsite, we need something like `asyncio.get_running_loop` that works for _both Python and Rust_. In Python, `asyncio.get_running_loop` uses thread-local data to retrieve the event loop associated with the current thread. What we need in Rust is something that can retrieve the Python event loop associated with the current _task_.
|
||||||
|
|
||||||
|
Enter `pyo3_asyncio::<runtime>::get_current_loop`. This function first checks task-local data for a Python event loop, then falls back on `asyncio.get_running_loop` if no task-local event loop is found. This way both bases are covered.
|
||||||
|
|
||||||
|
Now, all we need is a way to store the event loop in task-local data. Since this is a runtime-specific feature, you can find the following functions in each runtime module:
|
||||||
|
|
||||||
|
- `pyo3_asyncio::<runtime>::scope` - Store the event loop in task-local data when executing the given Future.
|
||||||
|
- `pyo3_asyncio::<runtime>::scope_local` - Store the event loop in task-local data when executing the given `!Send` Future.
|
||||||
|
|
||||||
|
With these new functions, we can make our previous example more correct:
|
||||||
|
|
||||||
|
```rust no_run
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
#[pyfunction]
|
||||||
|
fn sleep(py: Python) -> PyResult<&PyAny> {
|
||||||
|
// get the current event loop through task-local data
|
||||||
|
// OR `asyncio.get_running_loop`
|
||||||
|
let current_loop = pyo3_asyncio::tokio::get_current_loop(py)?;
|
||||||
|
|
||||||
|
pyo3_asyncio::tokio::future_into_py_with_loop(
|
||||||
|
current_loop,
|
||||||
|
// Store the current loop in task-local data
|
||||||
|
pyo3_asyncio::tokio::scope(current_loop.into(), async move {
|
||||||
|
let py_sleep = Python::with_gil(|py| {
|
||||||
|
pyo3_asyncio::into_future_with_loop(
|
||||||
|
// Now we can get the current loop through task-local data
|
||||||
|
pyo3_asyncio::tokio::get_current_loop(py)?,
|
||||||
|
py.import("asyncio")?.call_method1("sleep", (1,))?
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
py_sleep.await?;
|
||||||
|
|
||||||
|
Ok(Python::with_gil(|py| py.None()))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pyfunction]
|
||||||
|
fn wrap_sleep(py: Python) -> PyResult<&PyAny> {
|
||||||
|
// get the current event loop through task-local data
|
||||||
|
// OR `asyncio.get_running_loop`
|
||||||
|
let current_loop = pyo3_asyncio::tokio::get_current_loop(py)?;
|
||||||
|
|
||||||
|
pyo3_asyncio::tokio::future_into_py_with_loop(
|
||||||
|
current_loop,
|
||||||
|
// Store the current loop in task-local data
|
||||||
|
pyo3_asyncio::tokio::scope(current_loop.into(), async move {
|
||||||
|
let py_sleep = Python::with_gil(|py| {
|
||||||
|
pyo3_asyncio::into_future_with_loop(
|
||||||
|
pyo3_asyncio::tokio::get_current_loop(py)?,
|
||||||
|
// We can also call sleep within a Rust task since the
|
||||||
|
// event loop is stored in task local data
|
||||||
|
sleep(py)?
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
py_sleep.await?;
|
||||||
|
|
||||||
|
Ok(Python::with_gil(|py| py.None()))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymodule]
|
||||||
|
fn my_mod(py: Python, m: &PyModule) -> PyResult<()> {
|
||||||
|
m.add_function(wrap_pyfunction!(sleep, m)?)?;
|
||||||
|
m.add_function(wrap_pyfunction!(wrap_sleep, m)?)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Even though this is more correct, it's clearly not more ergonomic. That's why we introduced a new set of functions with this functionality baked in:
|
||||||
|
|
||||||
|
- `pyo3_asyncio::<runtime>::into_future`
|
||||||
|
> Convert a Python awaitable into a Rust future (using `pyo3_asyncio::<runtime>::get_current_loop`)
|
||||||
|
- `pyo3_asyncio::<runtime>::future_into_py`
|
||||||
|
> Convert a Rust future into a Python awaitable (using `pyo3_asyncio::<runtime>::get_current_loop` and `pyo3_asyncio::<runtime>::scope` to set the task-local event loop for the given Rust future)
|
||||||
|
- `pyo3_asyncio::<runtime>::local_future_into_py`
|
||||||
|
> Convert a `!Send` Rust future into a Python awaitable (using `pyo3_asyncio::<runtime>::get_current_loop` and `pyo3_asyncio::<runtime>::scope_local` to set the task-local event loop for the given Rust future).
|
||||||
|
|
||||||
|
__These are the functions that we recommend using__. With these functions, the previous example can be rewritten to be more compact:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
#[pyfunction]
|
||||||
|
fn sleep(py: Python) -> PyResult<&PyAny> {
|
||||||
|
pyo3_asyncio::tokio::future_into_py(py, async move {
|
||||||
|
let py_sleep = Python::with_gil(|py| {
|
||||||
|
pyo3_asyncio::tokio::into_future(
|
||||||
|
py.import("asyncio")?.call_method1("sleep", (1,))?
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
py_sleep.await?;
|
||||||
|
|
||||||
|
Ok(Python::with_gil(|py| py.None()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pyfunction]
|
||||||
|
fn wrap_sleep(py: Python) -> PyResult<&PyAny> {
|
||||||
|
pyo3_asyncio::tokio::future_into_py(py, async move {
|
||||||
|
let py_sleep = Python::with_gil(|py| {
|
||||||
|
pyo3_asyncio::tokio::into_future(sleep(py)?)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
py_sleep.await?;
|
||||||
|
|
||||||
|
Ok(Python::with_gil(|py| py.None()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymodule]
|
||||||
|
fn my_mod(py: Python, m: &PyModule) -> PyResult<()> {
|
||||||
|
m.add_function(wrap_pyfunction!(sleep, m)?)?;
|
||||||
|
m.add_function(wrap_pyfunction!(wrap_sleep, m)?)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### A Note for `v0.13` Users
|
||||||
|
|
||||||
|
Hey guys, I realize that these are pretty major changes for `v0.14`, and I apologize in advance for having to modify the public API so much. I hope
|
||||||
|
the explanation above gives some much needed context and justification for all the breaking changes.
|
||||||
|
|
||||||
|
Part of the reason why it's taken so long to push out a `v0.14` release is because I wanted to make sure we got this release right. There were a lot of issues with the `v0.13` release that I hadn't anticipated, and it's thanks to your feedback and patience that we've worked through these issues to get a more correct, more flexible version out there!
|
||||||
|
|
||||||
|
This new release should address most the core issues that users have reported in the `v0.13` release, so I think we can expect more stability going forward.
|
||||||
|
|
||||||
|
Also, a special thanks to [@ShadowJonathan](https://github.com/ShadowJonathan) for helping with the design and review
|
||||||
|
of these changes!
|
||||||
|
|
||||||
|
- [@awestlake87](https://github.com/awestlake87)
|
||||||
|
|
||||||
## PyO3 Asyncio in Cargo Tests
|
## PyO3 Asyncio in Cargo Tests
|
||||||
|
|
||||||
The default Cargo Test harness does not currently allow test crates to provide their own `main`
|
The default Cargo Test harness does not currently allow test crates to provide their own `main`
|
||||||
|
|
Loading…
Reference in a new issue