pyo3-async-runtimes

repository·main·Indexed 41 days ago

https://github.com/pyo3/pyo3-async-runtimes

Bridges Rust runtimes to Python's asyncio library, enabling interaction between Rust Futures and Python Coroutines. Version 0.29.0 supports popular Rust runtimes including Tokio and async-std, providing utilities to convert Python coroutines into Rust Futures and Rust Futures into Python awaitables, as well as specialized proc macros for async main functions and tests.

Tokens
7K
Snippets
19
Records
25
Agent score
51%

What's inside pyo3-async-runtimes

  1. How pyo3-async-runtimes facilitates async interoperability

    main

    The library acts as a bridge between Rust's async model (Futures) and Python's async model (Coroutines/Asyncio).

    • Rust $\rightarrow$ Python: When you want to call a Python async def function from Rust, you use into_future to turn the returned Python coroutine into a Rust Future. This allows you to use .await in your Rust code.
    • Python $\rightarrow$ Rust: When you want to expose a Rust async fn to Python, you use future_into_py to wrap the Rust Future into a Python object. This allows Python users to use the await keyword on your Rust-provided functions.

    All async Python code runs on the default asyncio event loop, and pyo3-async-runtimes manages the lifecycle of the corresponding event loops and integrates with popular Rust runtimes like tokio and async-std.

  2. Install pyo3-async-runtimes for PyO3 Native Modules

    main

    When building a native Python module (using cdylib), ensure your Cargo.toml includes the extension-module feature for pyo3 and the corresponding runtime feature for pyo3-async-runtimes.

    For async-std:

    [lib]
    name = "my_async_module"
    crate-type = ["cdylib"]
    
    [dependencies]
    pyo3 = { version = "0.29", features = ["extension-module"] }
    pyo3-async-runtimes = { version = "0.29", features = ["async-std-runtime"] }
    async-std = "1.13"

    For tokio:

    [lib]
    name = "my_async_module"
    crate-type = ["cdylib"]
    
    [dependencies]
    pyo3 = { version = "0.29", features = ["extension-module"] }
    pyo3-async-runtimes = { version = "0.29", features = ["tokio-runtime"] }
    tokio = "1.40"
  3. Use uvloop in PyO3 Native Extensions

    main

    You can use uvloop as the event loop in a PyO3 native extension. Since uvloop is a non-standard loop, you cannot use the #[pyo3_async_runtimes::<runtime>::main] proc macro for initialization. Instead, install uvloop in Python before calling your async functions.

    Steps:

    1. Define your async functions using pyo3_async_runtimes::tokio::future_into_py (or the async-std equivalent).
    2. In Python, call uvloop.install().
    3. Run your async code using asyncio.run().
    // lib.rs
    use pyo3::{prelude::*, wrap_pyfunction};
    
    #[pyfunction]
    fn rust_sleep(py: Python) -> PyResult<Bound<PyAny>> {
        pyo3_async_runtimes::tokio::future_into_py(py, async {
            tokio::time::sleep(std::time::Duration::from_secs(1)).await;
            Ok(())
        })
    }
    
    #[pymodule]
    fn my_async_module(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
        m.add_function(wrap_pyfunction!(rust_sleep, m)?)?;
        Ok(())
    }
    import asyncio
    import uvloop
    import my_async_module
    
    uvloop.install()
    
    async def main():
        await my_async_module.rust_sleep()
    
    asyncio.run(main())
  4. Install pyo3-async-runtimes for Rust Applications

    main

    To use pyo3-async-runtimes in a standalone Rust application, add the following dependencies to your Cargo.toml. You must select a runtime (either async-std-runtime or tokio-runtime) via features.

    For async-std:

    [dependencies]
    pyo3 = { version = "0.29" }
    pyo3-async-runtimes = { version = "0.29", features = ["attributes", "async-std-runtime"] }
    async-std = "1.13"

    For tokio:

    [dependencies]
    pyo3 = { version = "0.29" }
    pyo3-async-runtimes = { version = "0.29", features = ["attributes", "tokio-runtime"] }
    tokio = "1.40"
    # Example for async-std
    [dependencies]
    pyo3 = { version = "0.29" }
    pyo3-async-runtimes = { version = "0.29", features = ["attributes", "async-std-runtime"] }
    async-std = "1.13"
  5. Use uvloop in Rust Applications

    main

    To use uvloop in a standalone Rust application (not just a native extension), you must manually initialize Python and install the uvloop policy before starting the pyo3-async-runtimes loop. You cannot use the #[pyo3_async_runtimes::<runtime>::main] macro because the macro executes before you can install uvloop via Python.

    //! main.rs
    
    use pyo3::{prelude::*, types::PyType};
    
    fn main() -> PyResult<()> {
        Python::initialize();
    
        Python::attach(|py| {
            let uvloop = py.import("uvloop")?;
            uvloop.call_method0("install")?;
    
            // store a reference for the assertion
            let uvloop: Py<PyAny> = uvloop.into();
    
            pyo3_async_runtimes::async_std::run(py, async move {
                // verify that we are on a uvloop.Loop
                Python::attach(|py| -> PyResult<()> {
                    assert!(uvloop
                        .bind(py)
                        .getattr("Loop")?
                        .cast::<PyType>()
                        .unwrap()
                        .is_instance(&pyo3_async_runtimes::async_std::get_current_loop(py)?)
                        ?);
                    Ok(())
                })?;
    
                async_std::task::sleep(std::time::Duration::from_secs(1)).await;
    
                Ok(())
            })
        })
    }
  6. Initialize PyO3 with async-std or tokio using proc macros

    main

    Because Python must control the main thread, you cannot use standard runtime macros like #[tokio::main] or #[async_std::main] in a PyO3 application. Instead, use the pyo3-async-runtimes versions of these macros. These macros ensure the main thread blocks on pyo3_async_runtimes::<runtime>::run_until_complete while allowing Python to manage the main thread.

    To use async-std, use the #[pyo3_async_runtimes::async_std::main] attribute on your async fn main().

    use pyo3::prelude::*;
    
    #[pyo3_async_runtimes::async_std::main]
    async fn main() -> PyResult<()> {
        // PyO3 is initialized - Ready to go
    
        let fut = Python::attach(|py| -> PyResult<_> {
            let asyncio = py.import("asyncio")?;
    
            // convert asyncio.sleep into a Rust Future
            pyo3_async_runtimes::async_std::into_future(
                asyncio.call_method1("sleep", (1,))?
            )
        })?;
    
        fut.await?;
    
        Ok(())
    }
  7. Workaround for contextvars in synchronous functions within Rust futures

    main

    When running a Rust future that calls into Python, contextvars are preserved for async Python functions. However, synchronous Python functions called from the Rust thread will fail to resolve contextvars because they lack the coroutine context.

    Workaround:

    1. Retrieve the current task locals using get_current_locals::<R>(py).
    2. Access the context via TaskLocals::context.
    3. Wrap the synchronous function call in contextvars.Context.run to manually set the context, execute the function, and restore the previous context upon completion or exception.
  8. Use TaskLocals to manage Python event loops and contextvars

    main

    When performing manual conversions, you can use TaskLocals to explicitly carry the Python event loop and contextvars context into Rust. This is necessary for libraries that need to work with any event loop provided by the host application.

    Key methods:

    • TaskLocals::with_running_loop(py): Constructs locals using the current running loop via asyncio.get_running_loop().
    • TaskLocals::copy_context(py): Captures the current Python contextvars context.
    • TaskLocals::event_loop(py): Returns a reference to the stored event loop.
    • TaskLocals::context(py): Returns a reference to the stored context.
    use pyo3::{wrap_pyfunction, prelude::*};
    
    // #[cfg(feature = "tokio-runtime")]
    #[pyfunction]
    fn sleep(py: Python) -> PyResult<Bound<PyAny>> {
        // Construct the task locals structure with the current running loop and context
        let locals = pyo3_async_runtimes::TaskLocals::with_running_loop(py)?.copy_context(py)?;
    
        // Convert the async move { } block to a Python awaitable
        pyo3_async_runtimes::tokio::future_into_py_with_locals(py, locals.clone(), async move {
            let py_sleep = Python::attach(|py| {
                // Use the captured locals to ensure the inner Python call uses the correct loop/context
                pyo3_async_runtimes::into_future_with_locals(
                    &locals,
                    py.import("asyncio")?.call_method1("sleep", (1,))?
                )
            })?;
    
            py_sleep.await?;
    
            Ok(())
        })
    }
  9. How Python and Rust async models interop in pyo3-async-runtimes

    main
    The crate manages communication between separate Python and Rust event loops. Because Python's asyncio and Rust's async runtimes (like Tokio or Async-Std) have different requirements, they cannot run directly on each other's loops. Instead, this crate provides tools to convert Python awaitables into Rust futures and vice versa, ensuring that Python's contextvars and event loop associations are preserved during the transition.
  10. Configure pyo3-async-runtimes features

    main

    Depending on your requirements, you must enable specific Cargo features to use certain runtimes or utilities:

    FeatureDescription
    attributesEnables inventory re-exports for #[test] attributes
    async-std-runtimeEnables support for the async-std runtime
    tokio-runtimeEnables support for the tokio runtime
    testingEnables utilities for writing PyO3 Asyncio tests

    Example Cargo.toml configuration:

    [dependencies.pyo3-async-runtimes]
    version = "0.24"
    features = ["attributes", "tokio-runtime"]
  11. Avoid 'RuntimeError: no running event loop' when using asyncio.run()

    main

    When calling a Rust-defined async function from Python using asyncio.run(), you may encounter a RuntimeError: no running event loop. This happens because the Rust function is called (and attempts to access the event loop) before asyncio.run() has actually started the loop.

    Workaround: Instead of passing the Rust function directly to asyncio.run(), wrap it in a Python async def main() function and call asyncio.run(main()). This ensures the Rust function is only invoked once the event loop is active.

    import asyncio
    from my_async_module import rust_sleep
    
    # WRONG: This throws RuntimeError: no running event loop
    # asyncio.run(rust_sleep())
    
    # CORRECT: Wrap it in a coroutine
    async def main():
        await rust_sleep()
    
    asyncio.run(main())
  12. Convert a Python Coroutine into a Rust Future

    main

    To await a Python async function (which returns a coroutine object) within Rust, use pyo3_async_runtimes::<runtime>::into_future. This converts the Python coroutine into a Rust Future that can be .awaited.

    Example using tokio:

    use pyo3::prelude::*;
    
    #[pyo3_async_runtimes::tokio::main]
    async fn main() -> PyResult<()> {
        let future = Python::attach(|py| -> PyResult<_> {
            let example = py.import("example")?;
            // Calling the async function returns a coroutine
            let coroutine = example.call_method0("py_sleep")?;
            // Convert coroutine to Rust Future
            pyo3_async_runtimes::tokio::into_future(coroutine)
        })?;
    
        future.await?;
        Ok(())
    }
    use pyo3::prelude::*;
    
    #[pyo3_async_runtimes::tokio::main]
    async fn main() -> PyResult<()> {
        let future = Python::attach(|py| -> PyResult<_> {
            let example = py.import("example")?;
            let coroutine = example.call_method0("py_sleep")?;
            pyo3_async_runtimes::tokio::into_future(coroutine)
        })?;
    
        future.await?;
        Ok(())
    }