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