Skip to main content

arrow_pyarrow/
lib.rs

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