Skip to main content

arrow_pyarrow/
lib.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Pass Arrow objects from and to PyArrow, using Arrow's
19//! [C Data Interface](https://arrow.apache.org/docs/format/CDataInterface.html)
20//! and [pyo3](https://docs.rs/pyo3/latest/pyo3/).
21//!
22//! For underlying implementation, see the [ffi] module.
23//!
24//! One can use these to write Python functions that take and return PyArrow
25//! objects, with automatic conversion to corresponding arrow-rs types.
26//!
27//! ```ignore
28//! #[pyfunction]
29//! fn double_array(array: PyArrowType<ArrayData>) -> PyResult<PyArrowType<ArrayData>> {
30//!     let array = array.0; // Extract from PyArrowType wrapper
31//!     let array: Arc<dyn Array> = make_array(array); // Convert ArrayData to ArrayRef
32//!     let array: &Int32Array = array.as_any().downcast_ref()
33//!         .ok_or_else(|| PyValueError::new_err("expected int32 array"))?;
34//!     let array: Int32Array = array.iter().map(|x| x.map(|x| x * 2)).collect();
35//!     Ok(PyArrowType(array.into_data()))
36//! }
37//! ```
38//!
39//! | pyarrow type                | arrow-rs type                                                      |
40//! |-----------------------------|--------------------------------------------------------------------|
41//! | `pyarrow.DataType`          | [DataType]                                                         |
42//! | `pyarrow.Field`             | [Field]                                                            |
43//! | `pyarrow.Schema`            | [Schema]                                                           |
44//! | `pyarrow.Array`             | [ArrayData]                                                        |
45//! | `pyarrow.RecordBatch`       | [RecordBatch]                                                      |
46//! | `pyarrow.RecordBatchReader` | [RecordBatchStreamReader] / `Box<dyn RecordBatchReader + Send>` (1) |
47//! | `pyarrow.Table`             | [Table] (2)                                                        |
48//!
49//! (1) `pyarrow.RecordBatchReader` can be imported as [RecordBatchStreamReader]. Either
50//! [RecordBatchStreamReader] or `Box<dyn RecordBatchReader + Send>` can be exported
51//! as `pyarrow.RecordBatchReader`. (`Box<dyn RecordBatchReader + Send>` is typically
52//! easier to create.)
53//!
54//! (2) Although arrow-rs offers [Table], a convenience wrapper for [pyarrow.Table](https://arrow.apache.org/docs/python/generated/pyarrow.Table)
55//! that internally holds `Vec<RecordBatch>`, it is meant primarily for use cases where you already
56//! have `Vec<RecordBatch>` on the Rust side and want to export that in bulk as a `pyarrow.Table`.
57//! In general, it is recommended to use streaming approaches instead of dealing with data in bulk.
58//! For example, a `pyarrow.Table` (or any other object that implements the ArrayStream PyCapsule
59//! interface) can be imported to Rust through `PyArrowType<RecordBatchStreamReader>` instead of
60//! forcing eager reading into `Vec<RecordBatch>`.
61//!
62//! # Type stubs
63//!
64//! With the `experimental-inspect` feature enabled, each conversion records the pyarrow class it
65//! maps to in PyO3's introspection data, so a generated `.pyi` says `pyarrow.Array` where it would
66//! otherwise say `_typeshed.Incomplete`. The hints live on `FromPyArrow::INPUT_TYPE`,
67//! `ToPyArrow::OUTPUT_TYPE` and `IntoPyArrow::OUTPUT_TYPE`, and `PyArrowType` forwards them.
68//!
69//! Input hints name the pyarrow classes only, and are therefore narrower than what is accepted: the
70//! PyCapsule interface is duck-typed and has no canonical Python type to name.
71//!
72//! # Platform Support
73//!
74//! Only little-endian platforms are officially supported and tested in CI.
75//! Big-endian platforms are not tested in CI and may not work correctly.
76//! Fixes for big-endian platforms are welcome and handled on a best-effort basis,
77//! but compatibility is not guaranteed.
78
79use std::convert::{From, TryFrom};
80use std::ffi::CStr;
81use std::ptr::NonNull;
82use std::sync::Arc;
83
84use arrow_array::ffi;
85use arrow_array::ffi::{FFI_ArrowArray, FFI_ArrowSchema};
86use arrow_array::ffi_stream::{FFI_ArrowArrayStream, RecordBatchStreamReader};
87use arrow_array::{
88    RecordBatch, RecordBatchIterator, RecordBatchOptions, RecordBatchReader, StructArray,
89    make_array,
90};
91use arrow_data::ArrayData;
92use arrow_schema::{ArrowError, DataType, Field, Schema, SchemaRef};
93use pyo3::exceptions::{PyTypeError, PyValueError};
94use pyo3::ffi::Py_uintptr_t;
95use pyo3::prelude::*;
96use pyo3::sync::PyOnceLock;
97use pyo3::types::{PyCapsule, PyDict, PyList, PyString, PyType};
98use pyo3::{CastError, import_exception, intern};
99#[cfg(feature = "experimental-inspect")]
100use pyo3::{
101    inspect::PyStaticExpr, type_hint_identifier, type_hint_subscript, type_hint_union,
102    type_object::PyTypeInfo,
103};
104
105/// Declares a `FromPyArrow::INPUT_TYPE` / `ToPyArrow::OUTPUT_TYPE` / `IntoPyArrow::OUTPUT_TYPE`
106/// hint on an impl, expanding to nothing unless the `experimental-inspect` feature is enabled.
107macro_rules! type_hint {
108    ($name:ident = $hint:expr) => {
109        #[cfg(feature = "experimental-inspect")]
110        const $name: PyStaticExpr = $hint;
111    };
112}
113
114import_exception!(pyarrow, ArrowException);
115/// Represents an exception raised by PyArrow.
116pub type PyArrowException = ArrowException;
117
118fn to_py_err(err: ArrowError) -> PyErr {
119    PyArrowException::new_err(err.to_string())
120}
121
122/// The type hint shared by every conversion that imports through the ArrowArrayStream PyCapsule
123/// interface, i.e. [`RecordBatchStreamReader`] and [`Table`].
124///
125/// Both go through the same `__arrow_c_stream__` path and therefore accept exactly the same
126/// objects, so naming only one of the two classes would make a stub generator reject usage this
127/// crate's own documentation recommends — importing a `pyarrow.Table` as a
128/// `PyArrowType<RecordBatchStreamReader>`.
129#[cfg(feature = "experimental-inspect")]
130const ARRAY_STREAM_INPUT_TYPE: PyStaticExpr = type_hint_union!(
131    type_hint_identifier!("pyarrow", "RecordBatchReader"),
132    type_hint_identifier!("pyarrow", "Table")
133);
134
135/// Trait for converting Python objects to arrow-rs types without validation.
136///
137/// Prefer [`FromPyArrow`] where possible, as it validates the result before returning.
138pub trait FromPyArrowUnchecked: Sized {
139    /// Convert a Python object to an arrow-rs type without validating the result.
140    ///
141    /// # Safety
142    ///
143    /// The data exported by the Python object must satisfy all Arrow spec invariants
144    /// (valid offsets, buffer sizes, UTF-8, etc.) that [`ArrayData::validate_full`] checks.
145    unsafe fn from_pyarrow_bound_unchecked(value: &Bound<PyAny>) -> PyResult<Self>;
146}
147
148/// Trait for converting Python objects to arrow-rs types.
149pub trait FromPyArrow: Sized {
150    /// The Python type this conversion accepts, as a type hint.
151    ///
152    /// Used by [`FromPyObject::INPUT_TYPE`] on [`PyArrowType`] so that a stub generator can write
153    /// `pyarrow.Array` where it would otherwise write `_typeshed.Incomplete`.
154    ///
155    /// This names pyarrow classes only. Every conversion here *also* accepts any object
156    /// implementing the relevant [PyCapsule interface](https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html)
157    /// method, which is duck-typed and has no canonical Python type to point at — neither pyarrow
158    /// nor typeshed defines one. The hint is therefore narrower than what is accepted at runtime.
159    /// A binding that wants to advertise the wider protocol can declare its own `Protocol` and
160    /// carry it on a newtype around the arrow-rs type.
161    #[cfg(feature = "experimental-inspect")]
162    const INPUT_TYPE: PyStaticExpr = type_hint_identifier!("_typeshed", "Incomplete");
163
164    /// Convert a Python object to an arrow-rs type.
165    ///
166    /// Takes a GIL-bound value from Python and returns a result with the arrow-rs type.
167    fn from_pyarrow_bound(value: &Bound<PyAny>) -> PyResult<Self>;
168}
169
170/// Create a new PyArrow object from a arrow-rs type.
171pub trait ToPyArrow {
172    /// The Python type this conversion produces, as a type hint.
173    ///
174    /// Unlike [`FromPyArrow::INPUT_TYPE`] this is exact: the conversion always constructs an
175    /// instance of the named pyarrow class.
176    #[cfg(feature = "experimental-inspect")]
177    const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("_typeshed", "Incomplete");
178
179    /// Convert the implemented type into a Python object without consuming it.
180    fn to_pyarrow<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>>;
181}
182
183/// Convert an arrow-rs type into a PyArrow object.
184pub trait IntoPyArrow {
185    /// The Python type this conversion produces, as a type hint.
186    ///
187    /// See [`ToPyArrow::OUTPUT_TYPE`].
188    #[cfg(feature = "experimental-inspect")]
189    const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("_typeshed", "Incomplete");
190
191    /// Convert the implemented type into a Python object while consuming it.
192    fn into_pyarrow(self, py: Python<'_>) -> PyResult<Bound<'_, PyAny>>;
193}
194
195impl<T: ToPyArrow> IntoPyArrow for T {
196    type_hint!(OUTPUT_TYPE = <T as ToPyArrow>::OUTPUT_TYPE);
197
198    fn into_pyarrow(self, py: Python<'_>) -> PyResult<Bound<'_, PyAny>> {
199        self.to_pyarrow(py)
200    }
201}
202
203fn validate_class(expected: &Bound<PyType>, value: &Bound<PyAny>) -> PyResult<()> {
204    if !value.is_instance(expected)? {
205        return Err(PyTypeError::new_err(format!(
206            "Expected instance of {}, got {}",
207            expected.fully_qualified_name()?,
208            value.get_type().fully_qualified_name()?
209        )));
210    }
211    Ok(())
212}
213
214impl FromPyArrow for DataType {
215    type_hint!(INPUT_TYPE = type_hint_identifier!("pyarrow", "DataType"));
216
217    fn from_pyarrow_bound(value: &Bound<PyAny>) -> PyResult<Self> {
218        // Newer versions of PyArrow as well as other libraries with Arrow data implement this
219        // method, so prefer it over _export_to_c.
220        // See https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html
221        if let Some(capsule) =
222            call_capsule_method_if_exists(value, intern!(value.py(), "__arrow_c_schema__"))?
223        {
224            let schema_ptr = extract_capsule::<FFI_ArrowSchema>(
225                &capsule,
226                c"arrow_schema",
227                "__arrow_c_schema__",
228            )?;
229            return unsafe { DataType::try_from(schema_ptr.as_ref()) }.map_err(to_py_err);
230        }
231
232        validate_class(data_type_class(value.py())?, value)?;
233
234        let mut c_schema = FFI_ArrowSchema::empty();
235        value.call_method1(
236            intern!(value.py(), "_export_to_c"),
237            (&raw mut c_schema as Py_uintptr_t,),
238        )?;
239        DataType::try_from(&c_schema).map_err(to_py_err)
240    }
241}
242
243impl ToPyArrow for DataType {
244    type_hint!(OUTPUT_TYPE = type_hint_identifier!("pyarrow", "DataType"));
245
246    fn to_pyarrow<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
247        let c_schema = FFI_ArrowSchema::try_from(self).map_err(to_py_err)?;
248        data_type_class(py)?.call_method1(
249            intern!(py, "_import_from_c"),
250            (&raw const c_schema as Py_uintptr_t,),
251        )
252    }
253}
254
255impl FromPyArrow for Field {
256    type_hint!(INPUT_TYPE = type_hint_identifier!("pyarrow", "Field"));
257
258    fn from_pyarrow_bound(value: &Bound<PyAny>) -> PyResult<Self> {
259        // Newer versions of PyArrow as well as other libraries with Arrow data implement this
260        // method, so prefer it over _export_to_c.
261        // See https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html
262        if let Some(capsule) =
263            call_capsule_method_if_exists(value, intern!(value.py(), "__arrow_c_schema__"))?
264        {
265            let schema_ptr = extract_capsule::<FFI_ArrowSchema>(
266                &capsule,
267                c"arrow_schema",
268                "__arrow_c_schema__",
269            )?;
270            return unsafe { Field::try_from(schema_ptr.as_ref()) }.map_err(to_py_err);
271        }
272
273        validate_class(field_class(value.py())?, value)?;
274
275        let mut c_schema = FFI_ArrowSchema::empty();
276        value.call_method1(
277            intern!(value.py(), "_export_to_c"),
278            (&raw mut c_schema as Py_uintptr_t,),
279        )?;
280        Field::try_from(&c_schema).map_err(to_py_err)
281    }
282}
283
284impl ToPyArrow for Field {
285    type_hint!(OUTPUT_TYPE = type_hint_identifier!("pyarrow", "Field"));
286
287    fn to_pyarrow<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
288        let c_schema = FFI_ArrowSchema::try_from(self).map_err(to_py_err)?;
289        field_class(py)?.call_method1(
290            intern!(py, "_import_from_c"),
291            (&raw const c_schema as Py_uintptr_t,),
292        )
293    }
294}
295
296impl FromPyArrow for Schema {
297    type_hint!(INPUT_TYPE = type_hint_identifier!("pyarrow", "Schema"));
298
299    fn from_pyarrow_bound(value: &Bound<PyAny>) -> PyResult<Self> {
300        // Newer versions of PyArrow as well as other libraries with Arrow data implement this
301        // method, so prefer it over _export_to_c.
302        // See https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html
303        if let Some(capsule) =
304            call_capsule_method_if_exists(value, intern!(value.py(), "__arrow_c_schema__"))?
305        {
306            let schema_ptr = extract_capsule::<FFI_ArrowSchema>(
307                &capsule,
308                c"arrow_schema",
309                "__arrow_c_schema__",
310            )?;
311            return unsafe { Schema::try_from(schema_ptr.as_ref()) }.map_err(to_py_err);
312        }
313
314        validate_class(schema_class(value.py())?, value)?;
315
316        let mut c_schema = FFI_ArrowSchema::empty();
317        value.call_method1(
318            intern!(value.py(), "_export_to_c"),
319            (&raw mut c_schema as Py_uintptr_t,),
320        )?;
321        Schema::try_from(&c_schema).map_err(to_py_err)
322    }
323}
324
325impl ToPyArrow for Schema {
326    type_hint!(OUTPUT_TYPE = type_hint_identifier!("pyarrow", "Schema"));
327
328    fn to_pyarrow<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
329        let c_schema = FFI_ArrowSchema::try_from(self).map_err(to_py_err)?;
330        schema_class(py)?.call_method1(
331            intern!(py, "_import_from_c"),
332            (&raw const c_schema as Py_uintptr_t,),
333        )
334    }
335}
336
337impl FromPyArrowUnchecked for ArrayData {
338    unsafe fn from_pyarrow_bound_unchecked(value: &Bound<PyAny>) -> PyResult<Self> {
339        // Newer versions of PyArrow as well as other libraries with Arrow data implement this
340        // method, so prefer it over _export_to_c.
341        // See https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html
342        if let Some((schema_capsule, array_capsule)) = call_arrow_c_array_method_if_exists(value)? {
343            let schema_ptr =
344                extract_capsule(&schema_capsule, c"arrow_schema", "__arrow_c_array__")?;
345            let array_ptr = extract_capsule(&array_capsule, c"arrow_array", "__arrow_c_array__")?;
346            let array = unsafe { FFI_ArrowArray::from_raw(array_ptr.as_ptr()) };
347            return unsafe { ffi::from_ffi(array, schema_ptr.as_ref()) }.map_err(to_py_err);
348        }
349
350        validate_class(array_class(value.py())?, value)?;
351
352        // prepare a pointer to receive the Array struct
353        let mut array = FFI_ArrowArray::empty();
354        let mut schema = FFI_ArrowSchema::empty();
355
356        // make the conversion through PyArrow's private API
357        // this changes the pointer's memory and is thus unsafe.
358        // In particular, `_export_to_c` can go out of bounds
359        value.call_method1(
360            intern!(value.py(), "_export_to_c"),
361            (
362                &raw mut array as Py_uintptr_t,
363                &raw mut schema as Py_uintptr_t,
364            ),
365        )?;
366
367        unsafe { ffi::from_ffi(array, &schema) }.map_err(to_py_err)
368    }
369}
370
371impl FromPyArrow for ArrayData {
372    type_hint!(INPUT_TYPE = type_hint_identifier!("pyarrow", "Array"));
373
374    fn from_pyarrow_bound(value: &Bound<PyAny>) -> PyResult<Self> {
375        let data = unsafe { ArrayData::from_pyarrow_bound_unchecked(value)? };
376        data.validate_full().map_err(to_py_err)?;
377        Ok(data)
378    }
379}
380
381impl ToPyArrow for ArrayData {
382    type_hint!(OUTPUT_TYPE = type_hint_identifier!("pyarrow", "Array"));
383
384    fn to_pyarrow<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
385        let array = FFI_ArrowArray::new(self);
386        let schema = FFI_ArrowSchema::try_from(self.data_type()).map_err(to_py_err)?;
387        array_class(py)?.call_method1(
388            intern!(py, "_import_from_c"),
389            (
390                &raw const array as Py_uintptr_t,
391                &raw const schema as Py_uintptr_t,
392            ),
393        )
394    }
395}
396
397impl<T: FromPyArrow> FromPyArrow for Vec<T> {
398    type_hint!(
399        INPUT_TYPE = type_hint_subscript!(
400            type_hint_identifier!("collections.abc", "Iterable"),
401            <T as FromPyArrow>::INPUT_TYPE
402        )
403    );
404
405    fn from_pyarrow_bound(value: &Bound<PyAny>) -> PyResult<Self> {
406        let mut v = Vec::with_capacity(value.len().unwrap_or(0));
407        for item in value.try_iter()? {
408            v.push(T::from_pyarrow_bound(&item?)?);
409        }
410        Ok(v)
411    }
412}
413
414impl<T: ToPyArrow> ToPyArrow for Vec<T> {
415    type_hint!(
416        OUTPUT_TYPE = type_hint_subscript!(PyList::TYPE_HINT, <T as ToPyArrow>::OUTPUT_TYPE)
417    );
418
419    fn to_pyarrow<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
420        self.iter()
421            .map(|v| v.to_pyarrow(py))
422            .collect::<PyResult<Vec<_>>>()?
423            .into_pyobject(py)
424    }
425}
426
427fn record_batch_from_pyarrow_bound_impl(
428    value: &Bound<PyAny>,
429    validate: bool,
430) -> PyResult<RecordBatch> {
431    // Newer versions of PyArrow as well as other libraries with Arrow data implement this
432    // method, so prefer it over _export_to_c.
433    // See https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html
434    if let Some((schema_capsule, array_capsule)) = call_arrow_c_array_method_if_exists(value)? {
435        let schema_ptr = extract_capsule(&schema_capsule, c"arrow_schema", "__arrow_c_array__")?;
436        let array_ptr = extract_capsule(&array_capsule, c"arrow_array", "__arrow_c_array__")?;
437        let ffi_array = unsafe { FFI_ArrowArray::from_raw(array_ptr.as_ptr()) };
438        let array_data =
439            unsafe { ffi::from_ffi(ffi_array, schema_ptr.as_ref()) }.map_err(to_py_err)?;
440        if validate {
441            array_data.validate_full().map_err(to_py_err)?;
442        }
443        if !matches!(array_data.data_type(), DataType::Struct(_)) {
444            return Err(PyTypeError::new_err(format!(
445                "Expected Struct type from __arrow_c_array__, found {}.",
446                array_data.data_type()
447            )));
448        }
449        let options = RecordBatchOptions::default().with_row_count(Some(array_data.len()));
450        let array = StructArray::from(array_data);
451        // StructArray does not embed metadata from schema. We need to override
452        // the output schema with the schema from the capsule.
453        let schema = unsafe { Arc::new(Schema::try_from(schema_ptr.as_ref()).map_err(to_py_err)?) };
454        let (_fields, columns, nulls) = array.into_parts();
455        if nulls.map(|n| n.null_count()).unwrap_or_default() != 0 {
456            return Err(PyValueError::new_err(
457                "Cannot convert nullable StructArray to RecordBatch, see StructArray documentation",
458            ));
459        }
460        return RecordBatch::try_new_with_options(schema, columns, &options).map_err(to_py_err);
461    }
462
463    validate_class(record_batch_class(value.py())?, value)?;
464    // TODO(kszucs): implement the FFI conversions in arrow-rs for RecordBatches
465    let schema = value.getattr("schema")?;
466    let schema = Arc::new(Schema::from_pyarrow_bound(&schema)?);
467
468    let arrays = value.getattr("columns")?;
469    let arrays = arrays
470        .cast::<PyList>()?
471        .iter()
472        .map(|a| {
473            let data = unsafe { ArrayData::from_pyarrow_bound_unchecked(&a)? };
474            if validate {
475                data.validate_full().map_err(to_py_err)?;
476            }
477            Ok(make_array(data))
478        })
479        .collect::<PyResult<_>>()?;
480
481    let row_count = value
482        .getattr("num_rows")
483        .ok()
484        .and_then(|x| x.extract().ok());
485    let options = RecordBatchOptions::default().with_row_count(row_count);
486
487    RecordBatch::try_new_with_options(schema, arrays, &options).map_err(to_py_err)
488}
489
490impl FromPyArrow for RecordBatch {
491    type_hint!(INPUT_TYPE = type_hint_identifier!("pyarrow", "RecordBatch"));
492
493    fn from_pyarrow_bound(value: &Bound<PyAny>) -> PyResult<Self> {
494        record_batch_from_pyarrow_bound_impl(value, true)
495    }
496}
497
498impl FromPyArrowUnchecked for RecordBatch {
499    unsafe fn from_pyarrow_bound_unchecked(value: &Bound<PyAny>) -> PyResult<Self> {
500        record_batch_from_pyarrow_bound_impl(value, false)
501    }
502}
503
504impl ToPyArrow for RecordBatch {
505    type_hint!(OUTPUT_TYPE = type_hint_identifier!("pyarrow", "RecordBatch"));
506
507    fn to_pyarrow<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
508        // Workaround apache/arrow#37669 by returning RecordBatchIterator
509        let reader = RecordBatchIterator::new(vec![Ok(self.clone())], self.schema());
510        let reader: Box<dyn RecordBatchReader + Send> = Box::new(reader);
511        let py_reader = reader.into_pyarrow(py)?;
512        py_reader.call_method0(intern!(py, "read_next_batch"))
513    }
514}
515
516/// Supports conversion from `pyarrow.RecordBatchReader` to [RecordBatchStreamReader].
517impl FromPyArrow for RecordBatchStreamReader {
518    type_hint!(INPUT_TYPE = ARRAY_STREAM_INPUT_TYPE);
519
520    fn from_pyarrow_bound(value: &Bound<PyAny>) -> PyResult<Self> {
521        // Newer versions of PyArrow as well as other libraries with Arrow data implement this
522        // method, so prefer it over _export_to_c.
523        // See https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html
524        if let Some(capsule) =
525            call_capsule_method_if_exists(value, intern!(value.py(), "__arrow_c_stream__"))?
526        {
527            let stream_ptr =
528                extract_capsule(&capsule, c"arrow_array_stream", "__arrow_c_stream__")?;
529            let stream = unsafe { FFI_ArrowArrayStream::from_raw(stream_ptr.as_ptr()) };
530
531            let stream_reader = RecordBatchStreamReader::try_new(stream)
532                .map_err(|err| PyValueError::new_err(err.to_string()))?;
533
534            return Ok(stream_reader);
535        }
536
537        validate_class(record_batch_reader_class(value.py())?, value)?;
538
539        // prepare the stream struct to receive the content
540        let mut stream = FFI_ArrowArrayStream::empty();
541
542        // make the conversion through PyArrow's private API
543        // this changes the pointer's memory and is thus unsafe.
544        // In particular, `_export_to_c` can go out of bounds
545        value.call_method1(
546            intern!(value.py(), "_export_to_c"),
547            (&raw mut stream as Py_uintptr_t,),
548        )?;
549
550        RecordBatchStreamReader::try_new(stream)
551            .map_err(|err| PyValueError::new_err(err.to_string()))
552    }
553}
554
555/// Convert a [`RecordBatchReader`] into a `pyarrow.RecordBatchReader`.
556impl IntoPyArrow for Box<dyn RecordBatchReader + Send> {
557    type_hint!(OUTPUT_TYPE = type_hint_identifier!("pyarrow", "RecordBatchReader"));
558
559    // We can't implement `ToPyArrow` for `T: RecordBatchReader + Send` because
560    // there is already a blanket implementation for `T: ToPyArrow`.
561    fn into_pyarrow(self, py: Python<'_>) -> PyResult<Bound<'_, PyAny>> {
562        let stream = FFI_ArrowArrayStream::new(self);
563        record_batch_reader_class(py)?.call_method1(
564            intern!(py, "_import_from_c"),
565            (&raw const stream as Py_uintptr_t,),
566        )
567    }
568}
569
570/// Convert a [`RecordBatchStreamReader`] into a `pyarrow.RecordBatchReader`.
571impl IntoPyArrow for RecordBatchStreamReader {
572    type_hint!(OUTPUT_TYPE = type_hint_identifier!("pyarrow", "RecordBatchReader"));
573
574    fn into_pyarrow(self, py: Python<'_>) -> PyResult<Bound<'_, PyAny>> {
575        let boxed: Box<dyn RecordBatchReader + Send> = Box::new(self);
576        boxed.into_pyarrow(py)
577    }
578}
579
580/// This is a convenience wrapper around `Vec<RecordBatch>` that tries to simplify conversion from
581/// and to `pyarrow.Table`.
582///
583/// This could be used in circumstances where you either want to consume a `pyarrow.Table` directly
584/// (although technically, since `pyarrow.Table` implements the ArrayStreamReader PyCapsule
585/// interface, one could also consume a `PyArrowType<RecordBatchStreamReader>` instead) or, more
586/// importantly, where one wants to export a `pyarrow.Table` from a `Vec<RecordBatch>` from the Rust
587/// side.
588///
589/// ```ignore
590/// #[pyfunction]
591/// fn return_table(...) -> PyResult<PyArrowType<Table>> {
592///     let batches: Vec<RecordBatch>;
593///     let schema: SchemaRef;
594///     PyArrowType(Table::try_new(batches, schema).map_err(|err| err.into_py_err(py))?)
595/// }
596/// ```
597#[derive(Clone)]
598pub struct Table {
599    record_batches: Vec<RecordBatch>,
600    schema: SchemaRef,
601}
602
603impl Table {
604    pub fn try_new(
605        record_batches: Vec<RecordBatch>,
606        schema: SchemaRef,
607    ) -> Result<Self, ArrowError> {
608        for record_batch in &record_batches {
609            if schema != record_batch.schema() {
610                return Err(ArrowError::SchemaError(format!(
611                    "All record batches must have the same schema. \
612                         Expected schema: {:?}, got schema: {:?}",
613                    schema,
614                    record_batch.schema()
615                )));
616            }
617        }
618        Ok(Self {
619            record_batches,
620            schema,
621        })
622    }
623
624    pub fn record_batches(&self) -> &[RecordBatch] {
625        &self.record_batches
626    }
627
628    pub fn schema(&self) -> SchemaRef {
629        self.schema.clone()
630    }
631
632    pub fn into_inner(self) -> (Vec<RecordBatch>, SchemaRef) {
633        (self.record_batches, self.schema)
634    }
635}
636
637impl TryFrom<Box<dyn RecordBatchReader>> for Table {
638    type Error = ArrowError;
639
640    fn try_from(value: Box<dyn RecordBatchReader>) -> Result<Self, ArrowError> {
641        let schema = value.schema();
642        let batches = value.collect::<Result<Vec<_>, _>>()?;
643        Self::try_new(batches, schema)
644    }
645}
646
647/// Convert a `pyarrow.Table` (or any other ArrowArrayStream compliant object) into [`Table`]
648impl FromPyArrow for Table {
649    type_hint!(INPUT_TYPE = ARRAY_STREAM_INPUT_TYPE);
650
651    fn from_pyarrow_bound(ob: &Bound<PyAny>) -> PyResult<Self> {
652        let reader: Box<dyn RecordBatchReader> =
653            Box::new(RecordBatchStreamReader::from_pyarrow_bound(ob)?);
654        Self::try_from(reader).map_err(|err| PyValueError::new_err(err.to_string()))
655    }
656}
657
658/// Convert a [`Table`] into `pyarrow.Table`.
659impl IntoPyArrow for Table {
660    type_hint!(OUTPUT_TYPE = type_hint_identifier!("pyarrow", "Table"));
661
662    fn into_pyarrow(self, py: Python) -> PyResult<Bound<PyAny>> {
663        let py_batches = PyList::new(py, self.record_batches.into_iter().map(PyArrowType))?;
664        let py_schema = PyArrowType(Arc::unwrap_or_clone(self.schema));
665
666        let kwargs = PyDict::new(py);
667        kwargs.set_item("schema", py_schema)?;
668
669        table_class(py)?.call_method("from_batches", (py_batches,), Some(&kwargs))
670    }
671}
672
673fn array_class(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
674    static TYPE: PyOnceLock<Py<PyType>> = PyOnceLock::new();
675    TYPE.import(py, "pyarrow", "Array")
676}
677
678fn record_batch_class(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
679    static TYPE: PyOnceLock<Py<PyType>> = PyOnceLock::new();
680    TYPE.import(py, "pyarrow", "RecordBatch")
681}
682
683fn record_batch_reader_class(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
684    static TYPE: PyOnceLock<Py<PyType>> = PyOnceLock::new();
685    TYPE.import(py, "pyarrow", "RecordBatchReader")
686}
687
688fn data_type_class(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
689    static TYPE: PyOnceLock<Py<PyType>> = PyOnceLock::new();
690    TYPE.import(py, "pyarrow", "DataType")
691}
692
693fn field_class(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
694    static TYPE: PyOnceLock<Py<PyType>> = PyOnceLock::new();
695    TYPE.import(py, "pyarrow", "Field")
696}
697
698fn schema_class(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
699    static TYPE: PyOnceLock<Py<PyType>> = PyOnceLock::new();
700    TYPE.import(py, "pyarrow", "Schema")
701}
702
703fn table_class(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
704    static TYPE: PyOnceLock<Py<PyType>> = PyOnceLock::new();
705    TYPE.import(py, "pyarrow", "Table")
706}
707
708/// A newtype wrapper for types implementing [`FromPyArrow`] or [`IntoPyArrow`].
709///
710/// When wrapped around a type `T: FromPyArrow`, it
711/// implements [`FromPyObject`] for the PyArrow objects. When wrapped around a
712/// `T: IntoPyArrow`, it implements `IntoPy<PyObject>` for the wrapped type.
713#[derive(Debug)]
714pub struct PyArrowType<T>(pub T);
715
716impl<T: FromPyArrow> FromPyObject<'_, '_> for PyArrowType<T> {
717    type Error = PyErr;
718
719    type_hint!(INPUT_TYPE = <T as FromPyArrow>::INPUT_TYPE);
720
721    fn extract(value: Borrowed<'_, '_, PyAny>) -> PyResult<Self> {
722        Ok(Self(T::from_pyarrow_bound(&value)?))
723    }
724}
725
726impl<'py, T: IntoPyArrow> IntoPyObject<'py> for PyArrowType<T> {
727    type Target = PyAny;
728
729    type Output = Bound<'py, Self::Target>;
730
731    type Error = PyErr;
732
733    type_hint!(OUTPUT_TYPE = <T as IntoPyArrow>::OUTPUT_TYPE);
734
735    fn into_pyobject(self, py: Python<'py>) -> PyResult<Self::Output> {
736        self.0.into_pyarrow(py)
737    }
738}
739
740impl<T> From<T> for PyArrowType<T> {
741    fn from(s: T) -> Self {
742        Self(s)
743    }
744}
745
746fn call_capsule_method_if_exists<'py>(
747    object: &Bound<'py, PyAny>,
748    method_name: &Bound<'py, PyString>,
749) -> PyResult<Option<Bound<'py, PyCapsule>>> {
750    let Some(method) = object.getattr_opt(method_name)? else {
751        return Ok(None);
752    };
753    Ok(Some(method.call0()?.extract().map_err(
754        |e: CastError| {
755            wrapping_type_error(
756                object.py(),
757                e.into(),
758                format!("Expected {method_name} to return a capsule."),
759            )
760        },
761    )?))
762}
763
764fn call_arrow_c_array_method_if_exists<'py>(
765    object: &Bound<'py, PyAny>,
766) -> PyResult<Option<(Bound<'py, PyCapsule>, Bound<'py, PyCapsule>)>> {
767    let Some(method) = object.getattr_opt(intern!(object.py(), "__arrow_c_array__"))? else {
768        return Ok(None);
769    };
770    Ok(Some(method.call0()?.extract().map_err(|e| {
771        wrapping_type_error(
772            object.py(),
773            e,
774            "Expected __arrow_c_array__ to return a tuple of (schema, array) capsules.".into(),
775        )
776    })?))
777}
778
779fn extract_capsule<T>(
780    capsule: &Bound<PyCapsule>,
781    capsule_name: &CStr,
782    method_name: &'static str,
783) -> PyResult<NonNull<T>> {
784    Ok(capsule
785        .pointer_checked(Some(capsule_name))
786        .map_err(|e| {
787            wrapping_type_error(
788                capsule.py(),
789                e,
790                format!(
791                    "Expected {method_name} to return a {} capsule.",
792                    capsule_name.to_str().unwrap(),
793                ),
794            )
795        })?
796        .cast::<T>())
797}
798
799fn wrapping_type_error(py: Python<'_>, error: PyErr, message: String) -> PyErr {
800    let e = PyTypeError::new_err(message);
801    e.set_cause(py, Some(error));
802    e
803}
804
805#[cfg(all(test, feature = "experimental-inspect"))]
806mod introspection_tests {
807    use super::*;
808    use pyo3::{FromPyObject, IntoPyObject};
809
810    /// The type hint a `PyArrowType<T>` argument is described by.
811    fn input_type<T: FromPyArrow>() -> String {
812        <PyArrowType<T> as FromPyObject<'_, '_>>::INPUT_TYPE.to_string()
813    }
814
815    /// The type hint a `PyArrowType<T>` return value is described by.
816    fn output_type<T: IntoPyArrow>() -> String
817    where
818        PyArrowType<T>: for<'py> IntoPyObject<'py>,
819    {
820        <PyArrowType<T> as IntoPyObject<'_>>::OUTPUT_TYPE.to_string()
821    }
822
823    #[test]
824    fn scalar_types_map_to_their_pyarrow_class() {
825        assert_eq!(input_type::<DataType>(), "pyarrow.DataType");
826        assert_eq!(output_type::<DataType>(), "pyarrow.DataType");
827        assert_eq!(input_type::<Field>(), "pyarrow.Field");
828        assert_eq!(output_type::<Field>(), "pyarrow.Field");
829        assert_eq!(input_type::<Schema>(), "pyarrow.Schema");
830        assert_eq!(output_type::<Schema>(), "pyarrow.Schema");
831        assert_eq!(input_type::<RecordBatch>(), "pyarrow.RecordBatch");
832        assert_eq!(output_type::<RecordBatch>(), "pyarrow.RecordBatch");
833    }
834
835    /// `ArrayData` is the one case where the arrow-rs name and the pyarrow name differ.
836    #[test]
837    fn array_data_maps_to_pyarrow_array() {
838        assert_eq!(input_type::<ArrayData>(), "pyarrow.Array");
839        assert_eq!(output_type::<ArrayData>(), "pyarrow.Array");
840    }
841
842    /// Asymmetric on purpose: `Vec<T>` is built from anything iterable, but is handed back as a
843    /// list.
844    #[test]
845    fn vec_is_iterable_in_and_list_out() {
846        assert_eq!(
847            input_type::<Vec<RecordBatch>>(),
848            "collections.abc.Iterable[pyarrow.RecordBatch]"
849        );
850        assert_eq!(
851            output_type::<Vec<RecordBatch>>(),
852            "builtins.list[pyarrow.RecordBatch]"
853        );
854    }
855
856    /// Outputs are exact, but both stream imports accept either class, because both go through
857    /// `__arrow_c_stream__`.
858    #[test]
859    fn readers_and_tables_map_to_their_pyarrow_class() {
860        assert_eq!(
861            input_type::<RecordBatchStreamReader>(),
862            "pyarrow.RecordBatchReader | pyarrow.Table"
863        );
864        assert_eq!(
865            output_type::<RecordBatchStreamReader>(),
866            "pyarrow.RecordBatchReader"
867        );
868        assert_eq!(
869            output_type::<Box<dyn RecordBatchReader + Send>>(),
870            "pyarrow.RecordBatchReader"
871        );
872        assert_eq!(
873            input_type::<Table>(),
874            "pyarrow.RecordBatchReader | pyarrow.Table"
875        );
876        assert_eq!(output_type::<Table>(), "pyarrow.Table");
877    }
878}