This is an automated email from the ASF dual-hosted git repository.
paleolimbot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-nanoarrow.git
The following commit(s) were added to refs/heads/main by this push:
new 65d20676 fix(python): Support iterating map arrays as Python objects
(#998)
65d20676 is described below
commit 65d2067606289e5dc3d1e25f7dbf27c224cf04ad
Author: Edgar Ramírez Mondragón
<[email protected]>
AuthorDate: Thu Oct 29 10:43:34 2026 -0600
fix(python): Support iterating map arrays as Python objects (#998)
Adds support for iterating map arrays:
```python
# mre_map.py
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "nanoarrow",
# "pyarrow",
# ]
# ///
"""Iterating a map array with nanoarrow fails."""
import nanoarrow as na
import pyarrow as pa
array = pa.array(
[[("a", 1), ("b", None)], [], None, [("c", 3)]],
type=pa.map_(pa.string(), pa.int64()),
)
print(f"nanoarrow {na.__version__}, pyarrow {pa.__version__}")
print(na.Array(array).to_pylist())
```
Run it with `uv run --no-project mre_map.py`. The output is:
```
nanoarrow 0.9.0, pyarrow 26.0.0
...
KeyError: "Can't resolve iterator for type 'map'"
```
---------
Signed-off-by: Edgar Ramírez Mondragón <[email protected]>
---
python/src/nanoarrow/iterator.py | 23 +++++++++++++++++++++++
python/tests/test_iterator.py | 17 +++++++++++++++++
2 files changed, 40 insertions(+)
diff --git a/python/src/nanoarrow/iterator.py b/python/src/nanoarrow/iterator.py
index 6b66e9eb..0d577b77 100644
--- a/python/src/nanoarrow/iterator.py
+++ b/python/src/nanoarrow/iterator.py
@@ -281,6 +281,28 @@ class PyIterator(ArrayViewBaseIterator):
for start, end in zip(starts, ends):
yield list(islice(child_iter, end - start))
+ def _map_iter(self, offset: int, length: int):
+ if length == 0:
+ return
+
+ view = self._array_view
+ offset += view.offset
+
+ offsets = memoryview(view.buffer(1))[offset : (offset + length + 1)]
+ starts = offsets[:-1]
+ ends = offsets[1:]
+ entries = self._children[0]
+ entries_iter = entries._struct_tuple_iter(starts[0], ends[-1] -
starts[0])
+
+ if self._contains_nulls():
+ validity = view.buffer(0).elements(offset, length)
+ for is_valid, start, end in zip(validity, starts, ends):
+ item = list(islice(entries_iter, end - start))
+ yield item if is_valid else None
+ else:
+ for start, end in zip(starts, ends):
+ yield list(islice(entries_iter, end - start))
+
def _fixed_size_list_iter(self, offset, length):
view = self._array_view
offset += view.offset
@@ -625,6 +647,7 @@ _ITEMS_ITER_LOOKUP = {
_types.STRUCT: "_struct_iter",
_types.LIST: "_list_iter",
_types.LARGE_LIST: "_list_iter",
+ _types.MAP: "_map_iter",
_types.FIXED_SIZE_LIST: "_fixed_size_list_iter",
_types.SPARSE_UNION: "_sparse_union_iter",
_types.DENSE_UNION: "_dense_union_iter",
diff --git a/python/tests/test_iterator.py b/python/tests/test_iterator.py
index 8e62027b..b07a585d 100644
--- a/python/tests/test_iterator.py
+++ b/python/tests/test_iterator.py
@@ -278,6 +278,23 @@ def test_iterator_nullable_list():
assert (list(iter_py(nested_sliced))) == [[4, 5, 6], [7, 8, None], [0],
None]
+def test_iterator_map():
+ pa = pytest.importorskip("pyarrow")
+
+ array = pa.array([], pa.map_(pa.string(), pa.int64()))
+ assert list(iter_py(array)) == []
+
+ items = [[("a", 1), ("b", None)], [], None, [("c", 3)]]
+ array = pa.array(items, pa.map_(pa.string(), pa.int64()))
+ assert list(iter_py(array)) == items
+ assert list(iter_py(array[1:])) == items[1:]
+
+ # No nulls takes the non-nullable path
+ items = [[("a", 1)], [], [("b", 2), ("c", 3)]]
+ array = pa.array(items, pa.map_(pa.string(), pa.int64()))
+ assert list(iter_py(array)) == items
+
+
def test_iterator_fixed_size_list():
pa = pytest.importorskip("pyarrow")
items = [[1, 2, 3], [4, 5, 6], [7, 8, None]]