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}