pub fn local_future_into_py_with_locals<F, T>(
    py: Python<'_>,
    locals: TaskLocals,
    fut: F
) -> PyResult<&PyAny>where
    F: Future<Output = PyResult<T>> + 'static,
    T: IntoPy<PyObject>,
👎Deprecated since 0.18.0: Questionable whether these conversions have real-world utility (see https://github.com/awestlake87/pyo3-asyncio/issues/59#issuecomment-1008038497 and let me know if you disagree!)
Expand description

Convert a !Send Rust Future into a Python awaitable

If the asyncio.Future returned by this conversion is cancelled via asyncio.Future.cancel, the Rust future will be cancelled as well (new behaviour in v0.15).

Python contextvars are preserved when calling async Python functions within the Rust future via into_future (new behaviour in v0.15).

Although contextvars are preserved for async Python functions, synchronous functions will unfortunately fail to resolve them when called within the Rust future. This is because the function is being called from a Rust thread, not inside an actual Python coroutine context.

As a workaround, you can get the contextvars from the current task locals using get_current_locals and TaskLocals::context, then wrap your synchronous function in a call to contextvars.Context.run. This will set the context, call the synchronous function, and restore the previous context when it returns or raises an exception.

Arguments

  • py - PyO3 GIL guard
  • locals - The task locals for the given future
  • fut - The Rust future to be converted

Examples

use std::{rc::Rc, time::Duration};

use pyo3::prelude::*;

/// Awaitable non-send sleep function
#[pyfunction]
fn sleep_for(py: Python, secs: u64) -> PyResult<&PyAny> {
    // Rc is non-send so it cannot be passed into pyo3_asyncio::tokio::future_into_py
    let secs = Rc::new(secs);

    pyo3_asyncio::tokio::local_future_into_py_with_locals(
        py,
        pyo3_asyncio::tokio::get_current_locals(py)?,
        async move {
            tokio::time::sleep(Duration::from_secs(*secs)).await;
            Python::with_gil(|py| Ok(py.None()))
        }
    )
}

#[pyo3_asyncio::tokio::main]
async fn main() -> PyResult<()> {
    let locals = Python::with_gil(|py| -> PyResult<_> {
        pyo3_asyncio::tokio::get_current_locals(py)
    })?;

    // the main coroutine is running in a Send context, so we cannot use LocalSet here. Instead
    // we use spawn_blocking in order to use LocalSet::block_on
    tokio::task::spawn_blocking(move || {
        // LocalSet allows us to work with !Send futures within tokio. Without it, any calls to
        // pyo3_asyncio::tokio::local_future_into_py will panic.
        tokio::task::LocalSet::new().block_on(
            pyo3_asyncio::tokio::get_runtime(),
            pyo3_asyncio::tokio::scope_local(locals, async {
                Python::with_gil(|py| {
                    let py_future = sleep_for(py, 1)?;
                    pyo3_asyncio::tokio::into_future(py_future)
                })?
                .await?;

                Ok(())
            })
        )
    }).await.unwrap()
}