From 48f29ea7d6e40048bcc538aa388b861a4a88a447 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 7 Jan 2026 02:14:58 -0500 Subject: [PATCH] chore+feat(python-utils): update to pyo3 0.27, introduce helpers like `FromPyObjectViaParse` and `IntoPyObjectViaDisplay` before I make macros that can complement or replace them --- python-utils/Cargo.toml | 2 + python-utils/src/lib.rs | 169 +++++++++++++++++++++++++++++++-------- python-utils/src/none.rs | 27 +++++++ 3 files changed, 166 insertions(+), 32 deletions(-) create mode 100644 python-utils/src/none.rs diff --git a/python-utils/Cargo.toml b/python-utils/Cargo.toml index e8a5b3a..3726a7f 100644 --- a/python-utils/Cargo.toml +++ b/python-utils/Cargo.toml @@ -5,4 +5,6 @@ edition = "2021" license = { workspace = true } [dependencies] +derive_more = { workspace = true } pyo3 = { workspace = true } +snafu = { workspace = true } diff --git a/python-utils/src/lib.rs b/python-utils/src/lib.rs index 37ce5d9..a6319d2 100644 --- a/python-utils/src/lib.rs +++ b/python-utils/src/lib.rs @@ -1,45 +1,150 @@ -use std::convert::Infallible; +use std::{convert::Infallible, fmt::Display, str::FromStr, sync::Arc}; -use pyo3::{exceptions::PyTypeError, prelude::*, types::PyNone}; +use pyo3::{ + exceptions::{PyException, PyTypeError, PyValueError}, + prelude::*, + types::PyString, +}; +use snafu::{ResultExt, Snafu}; -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct IsNone; - -impl<'py> FromPyObject<'py> for IsNone { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { - ob.downcast::()?; - Ok(IsNone) - } -} - -impl<'py> IntoPyObject<'py> for IsNone { - type Target = PyNone; - - type Output = Borrowed<'py, 'py, Self::Target>; - - type Error = Infallible; - - fn into_pyobject(self, py: Python<'py>) -> Result { - Ok(PyNone::get(py)) - } -} +pub mod none; +pub use none::IsNone; /// Create a GIL-independent reference -pub fn detach(bound: &Bound) -> Py { - let py = bound.py(); - bound.as_unbound().clone_ref(py) +pub fn detach(borrowed: Borrowed<'_, '_, T>) -> Py { + let py = borrowed.py(); + borrowed.as_unbound().clone_ref(py) +} +/// Create a GIL-independent reference +pub fn detach_bound(bound: &Bound) -> Py { + detach(bound.as_borrowed()) } -pub fn validate_type_by_name(bound: &Bound, expected_type_name: &str) -> PyResult<()> { +#[derive(Debug, Snafu)] +pub enum TypeByNameValidationError { + /// error getting the type name of this object + GetTypeNameError { source: PyErr }, + /// error extracting the (successfully retrieved) type name as an [`&str`] + ExtractTypeNameError { source: PyErr }, + + /// error getting the fully qualified type name of this object + GetFullyQualifiedTypeNameError { source: PyErr }, + /// error extracting the (successfully retrieved) fully qualified type name as an [`&str`] + ExtractFullyQualifiedTypeNameError { source: PyErr }, + + /// expected an instance of {expected} but got an instance of {actual} + UnexpectedType { expected: String, actual: String }, +} + +impl From for PyErr { + fn from(error: TypeByNameValidationError) -> Self { + match &error { + TypeByNameValidationError::GetTypeNameError { .. } => { + PyException::new_err(error.to_string()) + } + TypeByNameValidationError::ExtractTypeNameError { .. } => { + PyException::new_err(error.to_string()) + } + TypeByNameValidationError::GetFullyQualifiedTypeNameError { .. } => { + PyException::new_err(error.to_string()) + } + TypeByNameValidationError::ExtractFullyQualifiedTypeNameError { .. } => { + PyException::new_err(error.to_string()) + } + TypeByNameValidationError::UnexpectedType { .. } => { + PyTypeError::new_err(error.to_string()) + } + } + } +} + +pub fn validate_type_by_name( + bound: &Bound, + expected_type_name: &str, +) -> Result<(), TypeByNameValidationError> { let py_type = bound.get_type(); - let type_name = py_type.name()?; - let type_name = type_name.to_str()?; + let type_name = py_type.name().context(GetTypeNameSnafu)?; + let type_name = type_name.to_str().context(ExtractTypeNameSnafu)?; if type_name != expected_type_name { - let fully_qualified_type_name = py_type.fully_qualified_name()?; - let fully_qualified_type_name = fully_qualified_type_name.to_str()?; - return Err(PyTypeError::new_err(format!("expected an instance of {expected_type_name} but got an instance of {fully_qualified_type_name}"))); + let fully_qualified_type_name = py_type + .fully_qualified_name() + .context(GetFullyQualifiedTypeNameSnafu)?; + let fully_qualified_type_name = fully_qualified_type_name + .to_str() + .context(ExtractFullyQualifiedTypeNameSnafu)?; + return Err(TypeByNameValidationError::UnexpectedType { + expected: expected_type_name.to_owned(), + actual: fully_qualified_type_name.to_owned(), + }); } return Ok(()); } + +#[derive(Debug, Clone, derive_more::Display)] +pub struct IntoPyObjectViaDisplay(pub T); + +impl<'py, T> IntoPyObject<'py> for IntoPyObjectViaDisplay +where + T: Display, +{ + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let s = self.to_string(); + s.into_pyobject(py) + } +} + +#[derive(Debug, Clone, derive_more::FromStr)] +pub struct FromPyObjectViaParse(pub T); + +#[derive(Debug, Clone, Snafu)] +pub enum ExtractPyObjectViaParseError +where + ParseError: 'static + snafu::Error, +{ + /// couldn't extract the object as a string + ExtractStringError { source: Arc }, + + /// couldn't parse the string as an instance of this Rust type + ParseError { source: ParseError }, +} + +impl From> for PyErr +where + E: 'static + snafu::Error, +{ + fn from(error: ExtractPyObjectViaParseError) -> Self { + match &error { + ExtractPyObjectViaParseError::ExtractStringError { .. } => { + PyException::new_err(error.to_string()) + } + ExtractPyObjectViaParseError::ParseError { .. } => { + PyValueError::new_err(error.to_string()) + } + } + } +} + +impl<'a, 'py, T> FromPyObject<'a, 'py> for FromPyObjectViaParse +where + T: FromStr, + ::Err: 'static + snafu::Error, + PyErr: From::Err>>, +{ + type Error = ExtractPyObjectViaParseError<::Err>; + + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { + let s = obj + .extract::<&str>() + .map_err(Arc::new) + .context(ExtractStringSnafu)?; + let t = T::from_str(s).context(ParseSnafu)?; + + Ok(FromPyObjectViaParse(t)) + } +} diff --git a/python-utils/src/none.rs b/python-utils/src/none.rs new file mode 100644 index 0000000..f494b5c --- /dev/null +++ b/python-utils/src/none.rs @@ -0,0 +1,27 @@ +use std::convert::Infallible; + +use pyo3::{types::PyNone, Borrowed, FromPyObject, IntoPyObject, PyAny, PyErr, Python}; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct IsNone; + +impl<'a, 'py> FromPyObject<'a, 'py> for IsNone { + type Error = PyErr; + + fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result { + ob.cast::()?; + Ok(IsNone) + } +} + +impl<'py> IntoPyObject<'py> for IsNone { + type Target = PyNone; + + type Output = Borrowed<'py, 'py, Self::Target>; + + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(PyNone::get(py)) + } +}