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