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

This commit is contained in:
2026-01-07 02:14:58 -05:00
parent eff0ad2bf8
commit 48f29ea7d6
3 changed files with 166 additions and 32 deletions

View File

@@ -5,4 +5,6 @@ edition = "2021"
license = { workspace = true }
[dependencies]
derive_more = { workspace = true }
pyo3 = { workspace = true }
snafu = { workspace = true }

View File

@@ -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<Self> {
ob.downcast::<PyNone>()?;
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<Self::Output, Self::Error> {
Ok(PyNone::get(py))
}
}
pub mod none;
pub use none::IsNone;
/// Create a GIL-independent reference
pub fn detach<T>(bound: &Bound<T>) -> Py<T> {
let py = bound.py();
bound.as_unbound().clone_ref(py)
pub fn detach<T>(borrowed: Borrowed<'_, '_, T>) -> Py<T> {
let py = borrowed.py();
borrowed.as_unbound().clone_ref(py)
}
/// Create a GIL-independent reference
pub fn detach_bound<T>(bound: &Bound<T>) -> Py<T> {
detach(bound.as_borrowed())
}
pub fn validate_type_by_name(bound: &Bound<PyAny>, 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<TypeByNameValidationError> 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<PyAny>,
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<T>(pub T);
impl<'py, T> IntoPyObject<'py> for IntoPyObjectViaDisplay<T>
where
T: Display,
{
type Target = PyString;
type Output = Bound<'py, Self::Target>;
type Error = Infallible;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
let s = self.to_string();
s.into_pyobject(py)
}
}
#[derive(Debug, Clone, derive_more::FromStr)]
pub struct FromPyObjectViaParse<T>(pub T);
#[derive(Debug, Clone, Snafu)]
pub enum ExtractPyObjectViaParseError<ParseError>
where
ParseError: 'static + snafu::Error,
{
/// couldn't extract the object as a string
ExtractStringError { source: Arc<PyErr> },
/// couldn't parse the string as an instance of this Rust type
ParseError { source: ParseError },
}
impl<E> From<ExtractPyObjectViaParseError<E>> for PyErr
where
E: 'static + snafu::Error,
{
fn from(error: ExtractPyObjectViaParseError<E>) -> 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<T>
where
T: FromStr,
<T as FromStr>::Err: 'static + snafu::Error,
PyErr: From<ExtractPyObjectViaParseError<<T as FromStr>::Err>>,
{
type Error = ExtractPyObjectViaParseError<<T as FromStr>::Err>;
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
let s = obj
.extract::<&str>()
.map_err(Arc::new)
.context(ExtractStringSnafu)?;
let t = T::from_str(s).context(ParseSnafu)?;
Ok(FromPyObjectViaParse(t))
}
}

27
python-utils/src/none.rs Normal file
View File

@@ -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<Self, Self::Error> {
ob.cast::<PyNone>()?;
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<Self::Output, Self::Error> {
Ok(PyNone::get(py))
}
}