Crate pyo3_asyncio[][src]

Expand description

Rust Bindings to the Python Asyncio Event Loop

Motivation

This crate aims to provide a convenient interface to manage the interop between Python and Rust’s async/await models. It supports conversions between Rust and Python futures and manages the event loops for both languages. Python’s threading model and GIL can make this interop a bit trickier than one might expect, so there are a few caveats that users should be aware of.

Why Two Event Loops

Currently, we don’t have a way to run Rust futures directly on Python’s event loop. Likewise, Python’s coroutines cannot be directly spawned on a Rust event loop. The two coroutine models require some additional assistance from their event loops, so in all likelihood they will need a new unique event loop that addresses the needs of both languages if the coroutines are to be run on the same loop.

It’s not immediately clear that this would provide worthwhile performance wins either, so in the interest of keeping things simple, this crate creates and manages the Python event loop and handles the communication between separate Rust event loops.

Python’s Event Loop

Python is very picky about the threads used by the asyncio executor. In particular, it needs to have control over the main thread in order to handle signals like CTRL-C correctly. This means that Cargo’s default test harness will no longer work since it doesn’t provide a method of overriding the main function to add our event loop initialization and finalization.

Event Loop References

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.

use pyo3::{wrap_pyfunction, 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:

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:

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 for helping with the design and review of these changes!

Rust’s Event Loop

Currently only the async-std and Tokio runtimes are supported by this crate.

In the future, more runtimes may be supported for Rust.

Features

Items marked with attributes are only available when the attributes Cargo feature is enabled:

[dependencies.pyo3-asyncio]
version = "0.13"
features = ["attributes"]

Items marked with async-std-runtime are only available when the async-std-runtime Cargo feature is enabled:

[dependencies.pyo3-asyncio]
version = "0.13"
features = ["async-std-runtime"]

Items marked with tokio-runtime are only available when the tokio-runtime Cargo feature is enabled:

[dependencies.pyo3-asyncio]
version = "0.13"
features = ["tokio-runtime"]

Items marked with testing are only available when the testing Cargo feature is enabled:

[dependencies.pyo3-asyncio]
version = "0.13"
features = ["testing"]

Re-exports

pub use inventory;

Modules

async-std-runtime PyO3 Asyncio functions specific to the async-std runtime

Errors and exceptions related to PyO3 Asyncio

Generic implementations of PyO3 Asyncio utilities that can be used for any Rust runtime

testing Utilities for writing PyO3 Asyncio tests

tokio-runtime PyO3 Asyncio functions specific to the tokio runtime

Functions

get_event_loopDeprecated

Get a reference to the Python event loop cached by try_init (0.13 behaviour)

Get a reference to the Python Event Loop from Rust

into_futureDeprecated

Convert a Python awaitable into a Rust Future

Convert a Python awaitable into a Rust Future

run_foreverDeprecated

Run the event loop forever

try_closeDeprecated

Shutdown the event loops and perform any necessary cleanup

try_initDeprecated

Attempt to initialize the Python and Rust event loops

with_runtimeDeprecated

Wraps the provided function with the initialization and finalization for PyO3 Asyncio