From 1d7b6a79f28e20986ded3330aa81f81ff5de98cf Mon Sep 17 00:00:00 2001 From: Peter Bower <37089506+pbower@users.noreply.github.com> Date: Fri, 2 Oct 2026 04:24:25 +0100 Subject: [PATCH] Add Python argument wrappers for TextArrayV and Scalar --- minarrow-py/Cargo.lock | 2 +- minarrow-py/src/convert.rs | 32 ++++++++++++++++++++++ minarrow-py/src/lib.rs | 2 +- pyo3/Cargo.toml | 3 +++ pyo3/build.rs | 5 ++++ pyo3/src/lib.rs | 2 +- pyo3/src/types.rs | 55 +++++++++++++++++++++++++++++++++++++- 7 files changed, 97 insertions(+), 4 deletions(-) diff --git a/minarrow-py/Cargo.lock b/minarrow-py/Cargo.lock index 1676d205..7739aa87 100644 --- a/minarrow-py/Cargo.lock +++ b/minarrow-py/Cargo.lock @@ -37,7 +37,7 @@ dependencies = [ [[package]] name = "minarrow-py" -version = "0.18.2" +version = "0.18.3" dependencies = [ "minarrow", "minarrow-pyo3", diff --git a/minarrow-py/src/convert.rs b/minarrow-py/src/convert.rs index 6040f7f7..ee439cd0 100644 --- a/minarrow-py/src/convert.rs +++ b/minarrow-py/src/convert.rs @@ -23,6 +23,8 @@ use minarrow::{arr_i8_opt, arr_i16_opt, arr_u8_opt, arr_u16_opt}; use minarrow::enums::array::extract_option_values64; #[cfg(feature = "datetime")] use minarrow::enums::time_units::TimeUnit; +#[cfg(feature = "value_type")] +use minarrow::Value; #[cfg(feature = "decimal")] use minarrow::DecimalArray; #[cfg(feature = "datetime")] @@ -40,6 +42,7 @@ use pyo3::exceptions::{PyIndexError, PyTypeError, PyValueError}; use pyo3::ffi; use pyo3::prelude::*; use pyo3::types::{PyBool, PyString}; +use pyo3::Borrowed; use pyo3::IntoPyObjectExt; /// Reads a Python sequence into a single 64-byte aligned `Vec64` buffer. @@ -547,6 +550,35 @@ pub fn py_to_scalar(value: &Bound<'_, PyAny>) -> PyResult { )) } +/// A Minarrow `Scalar` extracted from a Python value through [`py_to_scalar`]. +/// +/// Used as a pyo3 parameter type where a function takes a `Scalar`, since +/// `FromPyObject` cannot be implemented for `Scalar` outside Minarrow. +#[repr(transparent)] +#[derive(Debug, Clone)] +pub struct PyScalar(pub Scalar); + +impl<'py> FromPyObject<'_, 'py> for PyScalar { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult { + py_to_scalar(&ob).map(PyScalar) + } +} + +impl From for Scalar { + fn from(value: PyScalar) -> Self { + value.0 + } +} + +#[cfg(feature = "value_type")] +impl From for Value { + fn from(value: PyScalar) -> Self { + Value::Scalar(value.0) + } +} + pub fn resolve_index(i: isize, len: usize) -> PyResult { let resolved = if i < 0 { i + len as isize } else { i }; if resolved < 0 || resolved as usize >= len { diff --git a/minarrow-py/src/lib.rs b/minarrow-py/src/lib.rs index 02e88f64..0828b507 100644 --- a/minarrow-py/src/lib.rs +++ b/minarrow-py/src/lib.rs @@ -52,7 +52,7 @@ pub use chunked_array::PyChunkedArray; #[cfg(feature = "ndarray")] pub use chunked_ndarray::{PyChunkedNdArray, PyChunkedNdArrayInner}; pub use chunked_table::PyChunkedTable; -pub use convert::{build_array, py_to_scalar, resolve_index, scalar_to_py}; +pub use convert::{build_array, py_to_scalar, resolve_index, scalar_to_py, PyScalar}; #[cfg(feature = "cube")] pub use cube::PyCube; pub use dtype::{dtype_from_arrow, width_from_arrow, DType, TypeClass}; diff --git a/pyo3/Cargo.toml b/pyo3/Cargo.toml index b7531a6c..5fd9cdbc 100644 --- a/pyo3/Cargo.toml +++ b/pyo3/Cargo.toml @@ -16,6 +16,9 @@ keywords = [ ] categories = ["external-ffi-bindings", "api-bindings"] description = "PyO3 bindings for MinArrow - zero-copy Arrow interop with Python via PyArrow" +# Gives dependent build scripts the source root as `DEP_MINARROW_PYO3_ROOT`, for +# generators that read the Python conversions with `syn`. +links = "minarrow_pyo3" [workspace] diff --git a/pyo3/build.rs b/pyo3/build.rs index 7d1fba79..95076142 100644 --- a/pyo3/build.rs +++ b/pyo3/build.rs @@ -14,4 +14,9 @@ fn main() { pyo3_build_config::add_extension_module_link_args(); + // The source root, published to dependent build scripts as `DEP_MINARROW_PYO3_ROOT`. + println!( + "cargo:root={}", + std::env::var("CARGO_MANIFEST_DIR").expect("cargo sets CARGO_MANIFEST_DIR") + ); } diff --git a/pyo3/src/lib.rs b/pyo3/src/lib.rs index 2180f21a..9b78c89e 100644 --- a/pyo3/src/lib.rs +++ b/pyo3/src/lib.rs @@ -120,7 +120,7 @@ use crate::ffi::dlpack::PyNdArrayInner; pub use error::{PyMinarrowError, PyMinarrowResult}; pub use types::{ PyArray, PyArrayView, PyChunkedArray, PyChunkedArrayView, PyField, PyRecordBatch, - PyRecordBatchView, PyTable, PyTableView, + PyRecordBatchView, PyTable, PyTableView, PyTextArrayView, }; // Re-export minarrow types that users might need diff --git a/pyo3/src/types.rs b/pyo3/src/types.rs index 5f67bb7c..f44a7049 100644 --- a/pyo3/src/types.rs +++ b/pyo3/src/types.rs @@ -19,8 +19,9 @@ use minarrow::{ Array, ArrayV, Field, FieldArray, SuperArray, SuperArrayV, SuperTable, SuperTableV, Table, - TableV, + TableV, TextArrayV, }; +use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use pyo3::Borrowed; use std::sync::Arc; @@ -109,6 +110,12 @@ impl From for Arc { } } +impl From for Array { + fn from(value: PyArray) -> Self { + value.0.array + } +} + impl AsRef for PyArray { fn as_ref(&self) -> &Array { &self.0.array @@ -135,6 +142,52 @@ impl<'py> IntoPyObject<'py> for PyArray { } } +// PyTextArrayView - Wrapper around MinArrow's TextArrayV + +/// Transparent wrapper around MinArrow's [`TextArrayV`]. +/// +/// Extracts from a Python list of `str`, or from any object exporting a string +/// or categorical array through the Arrow PyCapsule interface, such as a +/// PyArrow string array. +/// +/// # Example (Rust) +/// ```ignore +/// use minarrow_pyo3::PyTextArrayView; +/// use minarrow::TextArrayV; +/// +/// #[pyfunction] +/// fn add_labels(labels: PyTextArrayView) -> PyResult<()> { +/// let labels: TextArrayV = labels.into(); +/// // Process... +/// Ok(()) +/// } +/// ``` +#[repr(transparent)] +#[derive(Debug, Clone)] +pub struct PyTextArrayView(pub TextArrayV); + +impl From for TextArrayV { + fn from(value: PyTextArrayView) -> Self { + value.0 + } +} + +impl<'py> FromPyObject<'_, 'py> for PyTextArrayView { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult { + if let Ok(strings) = ob.extract::>() { + return Ok(PyTextArrayView(TextArrayV::from(strings))); + } + match to_rust::array_to_rust(&ob)?.array { + Array::TextArray(text) => Ok(PyTextArrayView(TextArrayV::from(text))), + _ => Err(PyTypeError::new_err( + "expected a list of str or an Arrow string or categorical array", + )), + } + } +} + // PyRecordBatch - Wrapper around MinArrow's Table /// Transparent wrapper around MinArrow's Table.