This is an automated email from the ASF dual-hosted git repository.

Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 6b7d6b3279 feat(pyarrow): describe conversions in PyO3 introspection 
data (#10492)
6b7d6b3279 is described below

commit 6b7d6b3279ba4b4925e43bfc9b8a266fcf1660ad
Author: Jonas Dedden <[email protected]>
AuthorDate: Thu Aug 6 03:20:40 2026 +0200

    feat(pyarrow): describe conversions in PyO3 introspection data (#10492)
    
    # Which issue does this PR close?
    
    - Closes #10491.
    
    # Rationale for this change
    
    PyO3's `experimental-inspect` feature records the Python type of every
    conversion into the built binary, so that tools like `maturin
    generate-stubs` can emit a real `.pyi`. The type comes from the
    `INPUT_TYPE` / `OUTPUT_TYPE` associated constants on `FromPyObject` /
    `IntoPyObject`.
    
    `PyArrowType` implements both traits but leaves those constants at their
    default, `_typeshed.Incomplete`. Because `PyArrowType` is how bindings
    exchange *every* Arrow value with Python, that one default erases the
    entire public API of any such binding. A function like
    
    ```rust
    #[pyfunction]
    fn cast_record_batch(
        record_batch: PyArrowType<RecordBatch>,
        schema: PyArrowType<Schema>,
    ) -> PyResult<PyArrowType<RecordBatch>> { ... }
    ```
    
    generates
    
    ```python
    def cast_record_batch(record_batch: Incomplete, schema: Incomplete) -> 
Incomplete: ...
    ```
    
    # What changes are included in this PR?
    
    - A new `experimental-inspect` feature on `arrow-pyarrow`, which just
    turns on PyO3's. `arrow` gets a matching feature that implies `pyarrow`.
    - `FromPyArrow::INPUT_TYPE`, `ToPyArrow::OUTPUT_TYPE` and
    `IntoPyArrow::OUTPUT_TYPE`, defaulting to `_typeshed.Incomplete` so that
    out-of-tree implementors are unaffected.
    - Implementations for every type the crate converts: `DataType`,
    `Field`, `Schema`, `ArrayData` (→ `pyarrow.Array`), `RecordBatch`,
    `Vec<T>`, `ArrowArrayStreamReader`, `Box<dyn RecordBatchReader + Send>`
    and `Table`.
    - `PyArrowType`'s `FromPyObject` / `IntoPyObject` impls forward them.
    - Unit tests pinning the rendered hint for each type, and a note in the
    crate docs.
    
    Input and output are separate constants because they genuinely differ:
    `Vec<T>` is built from anything iterable but handed back as a `list`, so
    it is `collections.abc.Iterable[pyarrow.RecordBatch]` in and
    `list[pyarrow.RecordBatch]` out.
    
    # Are these changes tested?
    
    Yes. `arrow-pyarrow` gains unit tests asserting the rendered hint for
    every conversion, run with `cargo test -p arrow-pyarrow --features
    experimental-inspect`.
    
    Beyond that, the change was verified end to end against a real binding:
    with `arrow` patched to this branch and the binding's own code left on
    plain `PyArrowType`, `maturin generate-stubs` produces the signatures
    and the `from pyarrow import ...` line shown above.
    
    # Are there any user-facing changes?
    
    No behaviour changes, and nothing is compiled unless
    `experimental-inspect` is enabled. The traits gain associated constants,
    but they are defaulted and feature-gated, so existing implementations
    keep compiling untouched.
    
    ---------
---
 .github/workflows/integration.yml |   4 +
 Cargo.lock                        |  25 +++++
 arrow-pyarrow/Cargo.toml          |   5 +
 arrow-pyarrow/src/lib.rs          | 186 ++++++++++++++++++++++++++++++++++++++
 arrow/Cargo.toml                  |   3 +
 arrow/README.md                   |   1 +
 6 files changed, 224 insertions(+)

diff --git a/.github/workflows/integration.yml 
b/.github/workflows/integration.yml
index df22097e5b..dd9a04c324 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -230,6 +230,10 @@ jobs:
       - name: Run Rust tests
         run: |
           source venv/bin/activate
+          # arrow-pyarrow's own tests are feature-gated, so the workspace-wide 
`cargo test` in
+          # rust.yml (default features) does not build them. They need no 
interpreter at runtime,
+          # but pyo3 needs one to link against, hence the active venv.
+          cargo test -p arrow-pyarrow --all-features
           cd arrow-pyarrow-testing
           cargo test
       - name: Run Python tests
diff --git a/Cargo.lock b/Cargo.lock
index ee741323d0..40077980e6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2756,6 +2756,7 @@ dependencies = [
  "portable-atomic",
  "pyo3-build-config",
  "pyo3-ffi",
+ "pyo3-macros",
 ]
 
 [[package]]
@@ -2777,6 +2778,30 @@ dependencies = [
  "pyo3-build-config",
 ]
 
+[[package]]
+name = "pyo3-macros"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "91f9d455db760a9a0b0ddeaac25f1390b8a36ba73dfbda9f127cac6fc340d4d5"
+dependencies = [
+ "proc-macro2",
+ "pyo3-macros-backend",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "pyo3-macros-backend"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "e343bcec300ff262f5806a33a4e51b6d097a8a46435f512fcb83e95592581625"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
 [[package]]
 name = "quad-rand"
 version = "0.2.3"
diff --git a/arrow-pyarrow/Cargo.toml b/arrow-pyarrow/Cargo.toml
index 0051de8743..f7d6797c3f 100644
--- a/arrow-pyarrow/Cargo.toml
+++ b/arrow-pyarrow/Cargo.toml
@@ -35,6 +35,11 @@ bench = false
 [package.metadata.docs.rs]
 all-features = true
 
+[features]
+# Emit the Python type of each conversion into PyO3's introspection data, so 
that tools like
+# `maturin generate-stubs` can put a real type into a generated `.pyi` instead 
of `Incomplete`.
+experimental-inspect = ["pyo3/experimental-inspect"]
+
 [dependencies]
 arrow-array = { workspace = true, features = ["ffi"] }
 arrow-data = { workspace = true }
diff --git a/arrow-pyarrow/src/lib.rs b/arrow-pyarrow/src/lib.rs
index cfe51b807a..01d482e76d 100644
--- a/arrow-pyarrow/src/lib.rs
+++ b/arrow-pyarrow/src/lib.rs
@@ -58,6 +58,16 @@
 //! For example, a `pyarrow.Table` (or any other object that implements the 
ArrayStream PyCapsule
 //! interface) can be imported to Rust through 
`PyArrowType<ArrowArrayStreamReader>` instead of
 //! forcing eager reading into `Vec<RecordBatch>`.
+//!
+//! # Type stubs
+//!
+//! With the `experimental-inspect` feature enabled, each conversion records 
the pyarrow class it
+//! maps to in PyO3's introspection data, so a generated `.pyi` says 
`pyarrow.Array` where it would
+//! otherwise say `_typeshed.Incomplete`. The hints live on 
`FromPyArrow::INPUT_TYPE`,
+//! `ToPyArrow::OUTPUT_TYPE` and `IntoPyArrow::OUTPUT_TYPE`, and `PyArrowType` 
forwards them.
+//!
+//! Input hints name the pyarrow classes only, and are therefore narrower than 
what is accepted: the
+//! PyCapsule interface is duck-typed and has no canonical Python type to name.
 
 use std::convert::{From, TryFrom};
 use std::ffi::CStr;
@@ -79,6 +89,20 @@ use pyo3::prelude::*;
 use pyo3::sync::PyOnceLock;
 use pyo3::types::{PyCapsule, PyDict, PyList, PyString, PyType};
 use pyo3::{CastError, import_exception, intern};
+#[cfg(feature = "experimental-inspect")]
+use pyo3::{
+    inspect::PyStaticExpr, type_hint_identifier, type_hint_subscript, 
type_hint_union,
+    type_object::PyTypeInfo,
+};
+
+/// Declares a `FromPyArrow::INPUT_TYPE` / `ToPyArrow::OUTPUT_TYPE` / 
`IntoPyArrow::OUTPUT_TYPE`
+/// hint on an impl, expanding to nothing unless the `experimental-inspect` 
feature is enabled.
+macro_rules! type_hint {
+    ($name:ident = $hint:expr) => {
+        #[cfg(feature = "experimental-inspect")]
+        const $name: PyStaticExpr = $hint;
+    };
+}
 
 import_exception!(pyarrow, ArrowException);
 /// Represents an exception raised by PyArrow.
@@ -88,8 +112,35 @@ fn to_py_err(err: ArrowError) -> PyErr {
     PyArrowException::new_err(err.to_string())
 }
 
+/// The type hint shared by every conversion that imports through the 
ArrowArrayStream PyCapsule
+/// interface, i.e. [`ArrowArrayStreamReader`] and [`Table`].
+///
+/// Both go through the same `__arrow_c_stream__` path and therefore accept 
exactly the same
+/// objects, so naming only one of the two classes would make a stub generator 
reject usage this
+/// crate's own documentation recommends — importing a `pyarrow.Table` as a
+/// `PyArrowType<ArrowArrayStreamReader>`.
+#[cfg(feature = "experimental-inspect")]
+const ARRAY_STREAM_INPUT_TYPE: PyStaticExpr = type_hint_union!(
+    type_hint_identifier!("pyarrow", "RecordBatchReader"),
+    type_hint_identifier!("pyarrow", "Table")
+);
+
 /// Trait for converting Python objects to arrow-rs types.
 pub trait FromPyArrow: Sized {
+    /// The Python type this conversion accepts, as a type hint.
+    ///
+    /// Used by [`FromPyObject::INPUT_TYPE`] on [`PyArrowType`] so that a stub 
generator can write
+    /// `pyarrow.Array` where it would otherwise write `_typeshed.Incomplete`.
+    ///
+    /// This names pyarrow classes only. Every conversion here *also* accepts 
any object
+    /// implementing the relevant [PyCapsule 
interface](https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html)
+    /// method, which is duck-typed and has no canonical Python type to point 
at — neither pyarrow
+    /// nor typeshed defines one. The hint is therefore narrower than what is 
accepted at runtime.
+    /// A binding that wants to advertise the wider protocol can declare its 
own `Protocol` and
+    /// carry it on a newtype around the arrow-rs type.
+    #[cfg(feature = "experimental-inspect")]
+    const INPUT_TYPE: PyStaticExpr = type_hint_identifier!("_typeshed", 
"Incomplete");
+
     /// Convert a Python object to an arrow-rs type.
     ///
     /// Takes a GIL-bound value from Python and returns a result with the 
arrow-rs type.
@@ -98,17 +149,32 @@ pub trait FromPyArrow: Sized {
 
 /// Create a new PyArrow object from a arrow-rs type.
 pub trait ToPyArrow {
+    /// The Python type this conversion produces, as a type hint.
+    ///
+    /// Unlike [`FromPyArrow::INPUT_TYPE`] this is exact: the conversion 
always constructs an
+    /// instance of the named pyarrow class.
+    #[cfg(feature = "experimental-inspect")]
+    const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("_typeshed", 
"Incomplete");
+
     /// Convert the implemented type into a Python object without consuming it.
     fn to_pyarrow<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>>;
 }
 
 /// Convert an arrow-rs type into a PyArrow object.
 pub trait IntoPyArrow {
+    /// The Python type this conversion produces, as a type hint.
+    ///
+    /// See [`ToPyArrow::OUTPUT_TYPE`].
+    #[cfg(feature = "experimental-inspect")]
+    const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("_typeshed", 
"Incomplete");
+
     /// Convert the implemented type into a Python object while consuming it.
     fn into_pyarrow<'py>(self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>>;
 }
 
 impl<T: ToPyArrow> IntoPyArrow for T {
+    type_hint!(OUTPUT_TYPE = <T as ToPyArrow>::OUTPUT_TYPE);
+
     fn into_pyarrow<'py>(self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> 
{
         self.to_pyarrow(py)
     }
@@ -126,6 +192,8 @@ fn validate_class(expected: &Bound<PyType>, value: 
&Bound<PyAny>) -> PyResult<()
 }
 
 impl FromPyArrow for DataType {
+    type_hint!(INPUT_TYPE = type_hint_identifier!("pyarrow", "DataType"));
+
     fn from_pyarrow_bound(value: &Bound<PyAny>) -> PyResult<Self> {
         // Newer versions of PyArrow as well as other libraries with Arrow 
data implement this
         // method, so prefer it over _export_to_c.
@@ -153,6 +221,8 @@ impl FromPyArrow for DataType {
 }
 
 impl ToPyArrow for DataType {
+    type_hint!(OUTPUT_TYPE = type_hint_identifier!("pyarrow", "DataType"));
+
     fn to_pyarrow<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
         let c_schema = FFI_ArrowSchema::try_from(self).map_err(to_py_err)?;
         data_type_class(py)?.call_method1(
@@ -163,6 +233,8 @@ impl ToPyArrow for DataType {
 }
 
 impl FromPyArrow for Field {
+    type_hint!(INPUT_TYPE = type_hint_identifier!("pyarrow", "Field"));
+
     fn from_pyarrow_bound(value: &Bound<PyAny>) -> PyResult<Self> {
         // Newer versions of PyArrow as well as other libraries with Arrow 
data implement this
         // method, so prefer it over _export_to_c.
@@ -190,6 +262,8 @@ impl FromPyArrow for Field {
 }
 
 impl ToPyArrow for Field {
+    type_hint!(OUTPUT_TYPE = type_hint_identifier!("pyarrow", "Field"));
+
     fn to_pyarrow<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
         let c_schema = FFI_ArrowSchema::try_from(self).map_err(to_py_err)?;
         field_class(py)?.call_method1(
@@ -200,6 +274,8 @@ impl ToPyArrow for Field {
 }
 
 impl FromPyArrow for Schema {
+    type_hint!(INPUT_TYPE = type_hint_identifier!("pyarrow", "Schema"));
+
     fn from_pyarrow_bound(value: &Bound<PyAny>) -> PyResult<Self> {
         // Newer versions of PyArrow as well as other libraries with Arrow 
data implement this
         // method, so prefer it over _export_to_c.
@@ -227,6 +303,8 @@ impl FromPyArrow for Schema {
 }
 
 impl ToPyArrow for Schema {
+    type_hint!(OUTPUT_TYPE = type_hint_identifier!("pyarrow", "Schema"));
+
     fn to_pyarrow<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
         let c_schema = FFI_ArrowSchema::try_from(self).map_err(to_py_err)?;
         schema_class(py)?.call_method1(
@@ -237,6 +315,8 @@ impl ToPyArrow for Schema {
 }
 
 impl FromPyArrow for ArrayData {
+    type_hint!(INPUT_TYPE = type_hint_identifier!("pyarrow", "Array"));
+
     fn from_pyarrow_bound(value: &Bound<PyAny>) -> PyResult<Self> {
         // Newer versions of PyArrow as well as other libraries with Arrow 
data implement this
         // method, so prefer it over _export_to_c.
@@ -271,6 +351,8 @@ impl FromPyArrow for ArrayData {
 }
 
 impl ToPyArrow for ArrayData {
+    type_hint!(OUTPUT_TYPE = type_hint_identifier!("pyarrow", "Array"));
+
     fn to_pyarrow<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
         let array = FFI_ArrowArray::new(self);
         let schema = 
FFI_ArrowSchema::try_from(self.data_type()).map_err(to_py_err)?;
@@ -285,6 +367,13 @@ impl ToPyArrow for ArrayData {
 }
 
 impl<T: FromPyArrow> FromPyArrow for Vec<T> {
+    type_hint!(
+        INPUT_TYPE = type_hint_subscript!(
+            type_hint_identifier!("collections.abc", "Iterable"),
+            <T as FromPyArrow>::INPUT_TYPE
+        )
+    );
+
     fn from_pyarrow_bound(value: &Bound<PyAny>) -> PyResult<Self> {
         let mut v = Vec::with_capacity(value.len().unwrap_or(0));
         for item in value.try_iter()? {
@@ -295,6 +384,10 @@ impl<T: FromPyArrow> FromPyArrow for Vec<T> {
 }
 
 impl<T: ToPyArrow> ToPyArrow for Vec<T> {
+    type_hint!(
+        OUTPUT_TYPE = type_hint_subscript!(PyList::TYPE_HINT, <T as 
ToPyArrow>::OUTPUT_TYPE)
+    );
+
     fn to_pyarrow<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
         self.iter()
             .map(|v| v.to_pyarrow(py))
@@ -304,6 +397,8 @@ impl<T: ToPyArrow> ToPyArrow for Vec<T> {
 }
 
 impl FromPyArrow for RecordBatch {
+    type_hint!(INPUT_TYPE = type_hint_identifier!("pyarrow", "RecordBatch"));
+
     fn from_pyarrow_bound(value: &Bound<PyAny>) -> PyResult<Self> {
         // Newer versions of PyArrow as well as other libraries with Arrow 
data implement this
         // method, so prefer it over _export_to_c.
@@ -362,6 +457,8 @@ impl FromPyArrow for RecordBatch {
 }
 
 impl ToPyArrow for RecordBatch {
+    type_hint!(OUTPUT_TYPE = type_hint_identifier!("pyarrow", "RecordBatch"));
+
     fn to_pyarrow<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
         // Workaround apache/arrow#37669 by returning RecordBatchIterator
         let reader = RecordBatchIterator::new(vec![Ok(self.clone())], 
self.schema());
@@ -373,6 +470,8 @@ impl ToPyArrow for RecordBatch {
 
 /// Supports conversion from `pyarrow.RecordBatchReader` to 
[ArrowArrayStreamReader].
 impl FromPyArrow for ArrowArrayStreamReader {
+    type_hint!(INPUT_TYPE = ARRAY_STREAM_INPUT_TYPE);
+
     fn from_pyarrow_bound(value: &Bound<PyAny>) -> PyResult<Self> {
         // Newer versions of PyArrow as well as other libraries with Arrow 
data implement this
         // method, so prefer it over _export_to_c.
@@ -410,6 +509,8 @@ impl FromPyArrow for ArrowArrayStreamReader {
 
 /// Convert a [`RecordBatchReader`] into a `pyarrow.RecordBatchReader`.
 impl IntoPyArrow for Box<dyn RecordBatchReader + Send> {
+    type_hint!(OUTPUT_TYPE = type_hint_identifier!("pyarrow", 
"RecordBatchReader"));
+
     // We can't implement `ToPyArrow` for `T: RecordBatchReader + Send` because
     // there is already a blanket implementation for `T: ToPyArrow`.
     fn into_pyarrow<'py>(self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> 
{
@@ -423,6 +524,8 @@ impl IntoPyArrow for Box<dyn RecordBatchReader + Send> {
 
 /// Convert a [`ArrowArrayStreamReader`] into a `pyarrow.RecordBatchReader`.
 impl IntoPyArrow for ArrowArrayStreamReader {
+    type_hint!(OUTPUT_TYPE = type_hint_identifier!("pyarrow", 
"RecordBatchReader"));
+
     fn into_pyarrow<'py>(self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> 
{
         let boxed: Box<dyn RecordBatchReader + Send> = Box::new(self);
         boxed.into_pyarrow(py)
@@ -498,6 +601,8 @@ impl TryFrom<Box<dyn RecordBatchReader>> for Table {
 
 /// Convert a `pyarrow.Table` (or any other ArrowArrayStream compliant object) 
into [`Table`]
 impl FromPyArrow for Table {
+    type_hint!(INPUT_TYPE = ARRAY_STREAM_INPUT_TYPE);
+
     fn from_pyarrow_bound(ob: &Bound<PyAny>) -> PyResult<Self> {
         let reader: Box<dyn RecordBatchReader> =
             Box::new(ArrowArrayStreamReader::from_pyarrow_bound(ob)?);
@@ -507,6 +612,8 @@ impl FromPyArrow for Table {
 
 /// Convert a [`Table`] into `pyarrow.Table`.
 impl IntoPyArrow for Table {
+    type_hint!(OUTPUT_TYPE = type_hint_identifier!("pyarrow", "Table"));
+
     fn into_pyarrow(self, py: Python) -> PyResult<Bound<PyAny>> {
         let py_batches = PyList::new(py, 
self.record_batches.into_iter().map(PyArrowType))?;
         let py_schema = PyArrowType(Arc::unwrap_or_clone(self.schema));
@@ -564,6 +671,8 @@ pub struct PyArrowType<T>(pub T);
 impl<T: FromPyArrow> FromPyObject<'_, '_> for PyArrowType<T> {
     type Error = PyErr;
 
+    type_hint!(INPUT_TYPE = <T as FromPyArrow>::INPUT_TYPE);
+
     fn extract(value: Borrowed<'_, '_, PyAny>) -> PyResult<Self> {
         Ok(Self(T::from_pyarrow_bound(&value)?))
     }
@@ -576,6 +685,8 @@ impl<'py, T: IntoPyArrow> IntoPyObject<'py> for 
PyArrowType<T> {
 
     type Error = PyErr;
 
+    type_hint!(OUTPUT_TYPE = <T as IntoPyArrow>::OUTPUT_TYPE);
+
     fn into_pyobject(self, py: Python<'py>) -> PyResult<Self::Output> {
         self.0.into_pyarrow(py)
     }
@@ -645,3 +756,78 @@ fn wrapping_type_error(py: Python<'_>, error: PyErr, 
message: String) -> PyErr {
     e.set_cause(py, Some(error));
     e
 }
+
+#[cfg(all(test, feature = "experimental-inspect"))]
+mod introspection_tests {
+    use super::*;
+    use pyo3::{FromPyObject, IntoPyObject};
+
+    /// The type hint a `PyArrowType<T>` argument is described by.
+    fn input_type<T: FromPyArrow>() -> String {
+        <PyArrowType<T> as FromPyObject<'_, '_>>::INPUT_TYPE.to_string()
+    }
+
+    /// The type hint a `PyArrowType<T>` return value is described by.
+    fn output_type<T: IntoPyArrow>() -> String
+    where
+        PyArrowType<T>: for<'py> IntoPyObject<'py>,
+    {
+        <PyArrowType<T> as IntoPyObject<'_>>::OUTPUT_TYPE.to_string()
+    }
+
+    #[test]
+    fn scalar_types_map_to_their_pyarrow_class() {
+        assert_eq!(input_type::<DataType>(), "pyarrow.DataType");
+        assert_eq!(output_type::<DataType>(), "pyarrow.DataType");
+        assert_eq!(input_type::<Field>(), "pyarrow.Field");
+        assert_eq!(output_type::<Field>(), "pyarrow.Field");
+        assert_eq!(input_type::<Schema>(), "pyarrow.Schema");
+        assert_eq!(output_type::<Schema>(), "pyarrow.Schema");
+        assert_eq!(input_type::<RecordBatch>(), "pyarrow.RecordBatch");
+        assert_eq!(output_type::<RecordBatch>(), "pyarrow.RecordBatch");
+    }
+
+    /// `ArrayData` is the one case where the arrow-rs name and the pyarrow 
name differ.
+    #[test]
+    fn array_data_maps_to_pyarrow_array() {
+        assert_eq!(input_type::<ArrayData>(), "pyarrow.Array");
+        assert_eq!(output_type::<ArrayData>(), "pyarrow.Array");
+    }
+
+    /// Asymmetric on purpose: `Vec<T>` is built from anything iterable, but 
is handed back as a
+    /// list.
+    #[test]
+    fn vec_is_iterable_in_and_list_out() {
+        assert_eq!(
+            input_type::<Vec<RecordBatch>>(),
+            "collections.abc.Iterable[pyarrow.RecordBatch]"
+        );
+        assert_eq!(
+            output_type::<Vec<RecordBatch>>(),
+            "builtins.list[pyarrow.RecordBatch]"
+        );
+    }
+
+    /// Outputs are exact, but both stream imports accept either class, 
because both go through
+    /// `__arrow_c_stream__`.
+    #[test]
+    fn readers_and_tables_map_to_their_pyarrow_class() {
+        assert_eq!(
+            input_type::<ArrowArrayStreamReader>(),
+            "pyarrow.RecordBatchReader | pyarrow.Table"
+        );
+        assert_eq!(
+            output_type::<ArrowArrayStreamReader>(),
+            "pyarrow.RecordBatchReader"
+        );
+        assert_eq!(
+            output_type::<Box<dyn RecordBatchReader + Send>>(),
+            "pyarrow.RecordBatchReader"
+        );
+        assert_eq!(
+            input_type::<Table>(),
+            "pyarrow.RecordBatchReader | pyarrow.Table"
+        );
+        assert_eq!(output_type::<Table>(), "pyarrow.Table");
+    }
+}
diff --git a/arrow/Cargo.toml b/arrow/Cargo.toml
index 9dbc59fe5d..c4bab6fc3f 100644
--- a/arrow/Cargo.toml
+++ b/arrow/Cargo.toml
@@ -74,6 +74,9 @@ prettyprint = ["arrow-cast/prettyprint"]
 # target without assuming an environment containing JavaScript.
 test_utils = ["dep:rand", "dep:half"]
 pyarrow = ["ffi", "dep:arrow-pyarrow"]
+# Record the Python type of each pyarrow conversion in PyO3's introspection 
data, so that stub
+# generators emit e.g. `pyarrow.Array` rather than `_typeshed.Incomplete`. 
Implies `pyarrow`.
+pyarrow-experimental-inspect = ["pyarrow", 
"arrow-pyarrow/experimental-inspect"]
 # force_validate runs full data validation for all arrays that are created
 # this is not enabled by default as it is too computationally expensive
 # but is run as part of our CI checks
diff --git a/arrow/README.md b/arrow/README.md
index fb5f6b9ab2..02c7a0cb72 100644
--- a/arrow/README.md
+++ b/arrow/README.md
@@ -64,6 +64,7 @@ The `arrow` crate provides the following features which may 
be enabled in your `
 - `chrono-tz` - support of parsing timezone using 
[chrono-tz](https://docs.rs/chrono-tz/0.6.0/chrono_tz/)
 - `ffi` - bindings for the Arrow C [C Data 
Interface](https://arrow.apache.org/docs/format/CDataInterface.html)
 - `pyarrow` - bindings for pyo3 to call arrow-rs from python
+- `pyarrow-experimental-inspect` - record the pyarrow type of each conversion 
in PyO3's introspection data, so that stub generators emit e.g. `pyarrow.Array` 
rather than `_typeshed.Incomplete` (also enables `pyarrow`)
 - `canonical_extension_types` - definitions for [canonical extension 
types](https://arrow.apache.org/docs/format/CanonicalExtensions.html#format-canonical-extensions)
 - `async` - definitions for traits using `async`, intended to work with the 
async ecosystem
 

Reply via email to