Skip to main content

uni_plugin_pyo3/
error.rs

1//! Error types for the PyO3 loader.
2//!
3//! Mirrors the shape of `uni_plugin_rhai::RhaiError` and
4//! `uni_plugin_extism::ExtismError` so the five loaders surface
5//! comparable failure modes.
6
7use thiserror::Error;
8
9use uni_plugin::PluginError;
10
11/// Errors specific to the PyO3 loader.
12#[derive(Debug, Error)]
13#[non_exhaustive]
14pub enum PyPluginError {
15    /// The Python callable's signature was not recognized (unknown type
16    /// name in `args` / `returns`, or the callable could not be resolved
17    /// by name in the module namespace).
18    #[error("python callable signature unrecognized: {0}")]
19    SignatureUnrecognized(String),
20
21    /// A Python `Exception` was raised inside a plugin callable.
22    /// `traceback` carries the formatted Python traceback string.
23    #[error("python exception in `{qname}`: {message}\n{traceback}")]
24    PythonException {
25        /// Qualified plugin function name.
26        qname: String,
27        /// `repr(e)` of the Python exception.
28        message: String,
29        /// Formatted Python traceback (best effort).
30        traceback: String,
31    },
32
33    /// The plugin's declared manifest did not pass validation (missing
34    /// required field, unknown type name, duplicate qname).
35    #[error("python plugin manifest invalid: {0}")]
36    ManifestInvalid(String),
37
38    /// The host's `PluginRegistrar` rejected one of our adapters
39    /// (capability missing, qname namespace violation, duplicate).
40    #[error("python plugin registrar rejected: {0}")]
41    RegistrarRejected(String),
42
43    /// Conversion between an Arrow array and a PyArrow object (via the
44    /// PyCapsule C Data Interface) failed: unsupported type, capsule
45    /// shape mismatch, or null pointer.
46    #[error("arrow <-> pyarrow conversion failure: {0}")]
47    ArrowConversion(String),
48
49    /// Internal / unexpected error.
50    #[error("uni-plugin-pyo3 internal error: {0}")]
51    Internal(String),
52}
53
54#[cfg(feature = "pyo3")]
55impl From<pyo3::PyErr> for PyPluginError {
56    fn from(err: pyo3::PyErr) -> Self {
57        use pyo3::types::{PyAnyMethods, PyTracebackMethods};
58
59        // Best-effort traceback capture under the GIL; if the GIL is not
60        // held this falls back to `display`.
61        pyo3::Python::attach(|py| {
62            let traceback = err
63                .traceback(py)
64                .and_then(|tb| tb.format().ok())
65                .unwrap_or_default();
66            let value = err.value(py);
67            let message = value
68                .repr()
69                .map(|r| r.to_string())
70                .unwrap_or_else(|_| err.to_string());
71            PyPluginError::PythonException {
72                qname: String::from("<unknown>"),
73                message,
74                traceback,
75            }
76        })
77    }
78}
79
80#[cfg(feature = "pyo3")]
81impl PyPluginError {
82    /// Tag the embedded `<unknown>` qname with a specific qname when the
83    /// caller knows which plugin fn was running.
84    #[must_use]
85    pub fn with_qname(self, qname: impl Into<String>) -> Self {
86        match self {
87            PyPluginError::PythonException {
88                qname: _,
89                message,
90                traceback,
91            } => PyPluginError::PythonException {
92                qname: qname.into(),
93                message,
94                traceback,
95            },
96            other => other,
97        }
98    }
99}
100
101impl From<PluginError> for PyPluginError {
102    fn from(err: PluginError) -> Self {
103        match err {
104            PluginError::DuplicateRegistration(q) => {
105                PyPluginError::RegistrarRejected(format!("duplicate registration: {q}"))
106            }
107            PluginError::CapabilityRequired(c) => {
108                PyPluginError::RegistrarRejected(format!("registrar caps missing: {c:?}"))
109            }
110            other => PyPluginError::Internal(format!("registrar: {other}")),
111        }
112    }
113}