rigetti_pyo3/
sync.rs

1//! Macros that allow asynchronous Rust functions to be exported as synchronous Python functions.
2
3/// Spawn and block on a future using the pyo3 tokio runtime.
4/// Useful for returning a synchronous `PyResult`.
5///
6///
7/// When used like the following:
8/// ```rs
9/// async fn say_hello(name: String) -> String {
10///     format!("hello {name}")
11/// }
12///
13/// #[pyo3(name="say_hello")]
14/// pub fn py_say_hello(name: String) -> PyResult<String> {
15///     py_sync!(say_hello(name))
16/// }
17/// ```
18///
19/// Becomes the associated "synchronous" python call:
20/// ```py
21/// assert say_hello("Rigetti") == "hello Rigetti"
22/// ```
23#[macro_export]
24macro_rules! py_sync {
25    ($py: ident, $body: expr) => {{
26        $py.allow_threads(|| {
27            let runtime = ::pyo3_asyncio::tokio::get_runtime();
28            let handle = runtime.spawn($body);
29
30            runtime.block_on(async {
31                tokio::select! {
32                    result = handle => result.map_err(|err| ::pyo3::exceptions::PyRuntimeError::new_err(err.to_string()))?,
33                    signal_err = async {
34                        // A 100ms loop delay is a bit arbitrary, but seems to
35                        // balance CPU usage and SIGINT responsiveness well enough.
36                        let delay = ::std::time::Duration::from_millis(100);
37                        loop {
38                            ::pyo3::Python::with_gil(|py| {
39                                py.check_signals()
40                            })?;
41                            ::tokio::time::sleep(delay).await;
42                        }
43                    } => signal_err,
44                }
45            })
46        })
47    }};
48}
49
50/// Convert a rust future into a Python awaitable using
51/// `pyo3_asyncio::tokio::future_into_py`
52#[macro_export]
53macro_rules! py_async {
54    ($py: ident, $body: expr) => {
55        ::pyo3_asyncio::tokio::future_into_py($py, $body)
56    };
57}
58
59/// Given a single implementation of an async function,
60/// create that function as private and two pyfunctions
61/// named after it that can be used to invoke either
62/// blocking or async variants of the same function.
63///
64/// In order to export the function to Python using pyo3
65/// you must include the `#[pyfunction]` attribute. This
66/// isn't included in the macro by default since one may
67/// wish to annotate `#[pyfunction]` with additional
68/// arguments.
69///
70/// The given function will be spawned on a Rust event loop
71/// this means functions like [`pyo3::Python::with_gil`](pyo3::Python::with_gil)
72/// should not be used, as acquiring Python's global
73/// interpreter lock from a Rust runtime
74/// isn't possible.
75///
76/// This macro cannot be used when lifetime specifiers are
77/// required, or the pyfunction bodies need additional
78/// parameter handling besides simply calling out to
79/// the underlying `py_async` or `py_sync` macros.
80///
81/// ```rs
82/// // ... becomes python package "things"
83/// create_init_submodule! {
84///     funcs: [
85///         py_do_thing,
86///         py_do_thing_async,
87///     ]
88/// }
89///
90/// py_function_sync_async! {
91///     #[pyfunction]
92///     #[args(timeout = "None")]
93///     async fn do_thing(timeout: Option<u64>) -> PyResult<String> {
94///         // ... sleep for timeout ...
95///         Ok(String::from("done"))
96///     }
97/// }
98/// ```
99///
100/// becomes in python:
101/// ```py
102/// from things import do_thing, do_thing_async
103/// assert do_thing() == "done"
104/// assert await do_thing_async() == "done"
105/// ```
106#[macro_export]
107macro_rules! py_function_sync_async {
108    (
109        $(#[$meta: meta])+
110        async fn $name: ident($($(#[$arg_meta: meta])*$arg: ident : $kind: ty),* $(,)?) $(-> $ret: ty)? $body: block
111    ) => {
112        async fn $name($($arg: $kind,)*) $(-> $ret)? {
113            $body
114        }
115
116        ::paste::paste! {
117        $(#[$meta])+
118        #[allow(clippy::too_many_arguments, missing_docs)]
119        #[pyo3(name = $name "")]
120        pub(crate) fn [< py_ $name >](py: ::pyo3::Python<'_> $(, $(#[$arg_meta])*$arg: $kind)*) $(-> $ret)? {
121            $crate::py_sync!(py, $name($($arg),*))
122        }
123
124        $(#[$meta])+
125        #[pyo3(name = $name "_async")]
126        #[allow(clippy::too_many_arguments, missing_docs)]
127        pub(crate) fn [< py_ $name _async >](py: ::pyo3::Python<'_> $(, $(#[$arg_meta])*$arg: $kind)*) -> ::pyo3::PyResult<&::pyo3::PyAny> {
128            $crate::py_async!(py, $name($($arg),*))
129        }
130        }
131    };
132}