pyo3_anyio/
lib.rs

1// BSD 3-Clause License
2//
3// Copyright (c) 2022, Lucina
4// All rights reserved.
5//
6// Redistribution and use in source and binary forms, with or without
7// modification, are permitted provided that the following conditions are met:
8//
9// * Redistributions of source code must retain the above copyright notice, this
10//   list of conditions and the following disclaimer.
11//
12// * Redistributions in binary form must reproduce the above copyright notice,
13//   this list of conditions and the following disclaimer in the documentation
14//   and/or other materials provided with the distribution.
15//
16// * Neither the name of the copyright holder nor the names of its contributors
17//   may be used to endorse or promote products derived from this software
18//   without specific prior written permission.
19//
20// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30// POSSIBILITY OF SUCH DAMAGE.
31
32//! `PyO3` utility bindings for Anyio's event loop.
33
34#![deny(clippy::pedantic)]
35#![allow(clippy::borrow_deref_ref)] // Leads to a ton of false positives around args of py types.
36#![allow(clippy::missing_panics_doc)] // TODO: finalise and document the panics
37#![allow(clippy::used_underscore_binding)] // Doesn't work with macros
38#![warn(missing_docs)]
39
40use once_cell::sync::OnceCell as OnceLock;
41use pyo3::exceptions::PyRuntimeError;
42use pyo3::types::{PyDict, PyTuple};
43use pyo3::{IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject};
44pub mod any;
45mod asyncio;
46pub mod tokio;
47pub mod traits;
48mod trio;
49
50pub use crate::asyncio::Asyncio;
51use crate::traits::PyLoop;
52pub use crate::trio::Trio;
53
54
55static NONE: OnceLock<PyObject> = OnceLock::new();
56// TODO: switch to std::sync::OnceLock once https://github.com/rust-lang/rust/issues/74465 is done.
57static SYS_MODULES: OnceLock<PyObject> = OnceLock::new();
58
59
60pub(crate) fn import_none(py: Python) -> &PyAny {
61    NONE.get_or_init(|| py.None()).as_ref(py)
62}
63
64fn import_sys_modules(py: Python) -> PyResult<&PyAny> {
65    SYS_MODULES
66        .get_or_try_init(|| Ok(py.import("sys")?.getattr("modules")?.to_object(py)))
67        .map(|value| value.as_ref(py))
68}
69
70#[pyo3::pyclass]
71pub(crate) struct ContextWrap {
72    callback: PyObject,
73    context: Option<PyObject>,
74}
75
76impl ContextWrap {
77    fn py(context: Option<&PyAny>, callback: &PyAny) -> PyObject {
78        let py = callback.py();
79        Self {
80            callback: callback.to_object(py),
81            context: context.map(|value| value.to_object(py)),
82        }
83        .into_py(py)
84    }
85}
86
87#[pyo3::pymethods]
88impl ContextWrap {
89    #[args(callback, args, kwargs = "None")]
90    fn __call__(&self, py: Python, mut args: Vec<PyObject>, kwargs: Option<&PyDict>) -> PyResult<PyObject> {
91        if let Some(context) = self.context.as_ref() {
92            args.insert(0, self.callback.clone_ref(py));
93            context.call_method(py, "run", PyTuple::new(py, args), kwargs)
94        } else {
95            self.callback.call(py, PyTuple::new(py, args), kwargs)
96        }
97    }
98}
99
100/// Get the current thread's active event loop.
101///
102/// # Errors
103///
104/// This will return a `Err(pyo3::PyErr)` where the inner type is
105/// `pyo3::exceptions::PyRuntimeError` if there is no running event loop.
106pub fn get_running_loop(py: Python) -> PyResult<Box<dyn PyLoop>> {
107    // sys.modules is used here to avoid unnecessarily trying to import asyncio or
108    // Trio if it hasn't been imported yet or isn't installed.
109    let modules = import_sys_modules(py)?;
110
111    // TODO: switch to && let Some(loop) = ... once https://github.com/rust-lang/rust/pull/94927
112    // is merged and released
113    if modules.contains("asyncio")? {
114        if let Some(loop_) = Asyncio::get_running_loop(py)? {
115            return Ok(Box::new(loop_));
116        }
117    };
118
119    if modules.contains("trio")? {
120        if let Some(loop_) = Trio::get_running_loop(py)? {
121            return Ok(Box::new(loop_));
122        }
123    };
124
125    Err(PyRuntimeError::new_err("No running event loop"))
126}