Skip to content

(fix): ndim accessible as np.ndim on PandasExtensionArray #10414

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions xarray/coding/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@
# otherwise numpy unsigned ints will silently cast to the signed counterpart
fill_value = fill_value.item()
# passes if provided fill value fits in encoded on-disk type
new_fill = encoded_dtype.type(fill_value)

Check warning on line 237 in xarray/coding/variables.py

View workflow job for this annotation

GitHub Actions / ubuntu-latest py3.10 min-all-deps

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype)` will give the desired result (the cast overflows).

Check warning on line 237 in xarray/coding/variables.py

View workflow job for this annotation

GitHub Actions / ubuntu-latest py3.10 min-all-deps

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype)` will give the desired result (the cast overflows).

Check warning on line 237 in xarray/coding/variables.py

View workflow job for this annotation

GitHub Actions / ubuntu-latest py3.10 min-all-deps

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype)` will give the desired result (the cast overflows).

Check warning on line 237 in xarray/coding/variables.py

View workflow job for this annotation

GitHub Actions / ubuntu-latest py3.10 min-all-deps

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype)` will give the desired result (the cast overflows).

Check warning on line 237 in xarray/coding/variables.py

View workflow job for this annotation

GitHub Actions / ubuntu-latest py3.10 min-all-deps

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype)` will give the desired result (the cast overflows).

Check warning on line 237 in xarray/coding/variables.py

View workflow job for this annotation

GitHub Actions / ubuntu-latest py3.10 min-all-deps

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype)` will give the desired result (the cast overflows).

Check warning on line 237 in xarray/coding/variables.py

View workflow job for this annotation

GitHub Actions / ubuntu-latest py3.10 min-all-deps

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype)` will give the desired result (the cast overflows).

Check warning on line 237 in xarray/coding/variables.py

View workflow job for this annotation

GitHub Actions / ubuntu-latest py3.10 min-all-deps

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype)` will give the desired result (the cast overflows).

Check warning on line 237 in xarray/coding/variables.py

View workflow job for this annotation

GitHub Actions / ubuntu-latest py3.10 min-all-deps

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype)` will give the desired result (the cast overflows).

Check warning on line 237 in xarray/coding/variables.py

View workflow job for this annotation

GitHub Actions / ubuntu-latest py3.10 min-all-deps

NumPy will stop allowing conversion of out-of-bound Python integers to integer arrays. The conversion of 255 to int8 will fail in the future. For the old behavior, usually: np.array(value).astype(dtype)` will give the desired result (the cast overflows).
except OverflowError:
encoded_kind_str = "signed" if encoded_dtype.kind == "i" else "unsigned"
warnings.warn(
Expand Down Expand Up @@ -520,9 +520,9 @@

scale_factor = pop_to(attrs, encoding, "scale_factor", name=name)
add_offset = pop_to(attrs, encoding, "add_offset", name=name)
if np.ndim(scale_factor) > 0:
if duck_array_ops.ndim(scale_factor) > 0:
scale_factor = np.asarray(scale_factor).item()
if np.ndim(add_offset) > 0:
if duck_array_ops.ndim(add_offset) > 0:
add_offset = np.asarray(add_offset).item()
# if we have a _FillValue/masked_value in encoding we already have the wanted
# floating point dtype here (via CFMaskCoder), so no check is necessary
Expand Down
6 changes: 6 additions & 0 deletions xarray/core/duck_array_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@

if xp == np:
# numpy currently doesn't have a astype:
return data.astype(dtype, **kwargs)

Check warning on line 253 in xarray/core/duck_array_ops.py

View workflow job for this annotation

GitHub Actions / macos-latest py3.10

invalid value encountered in cast

Check warning on line 253 in xarray/core/duck_array_ops.py

View workflow job for this annotation

GitHub Actions / macos-latest py3.10

invalid value encountered in cast
return xp.astype(data, dtype, **kwargs)


Expand Down Expand Up @@ -796,6 +796,12 @@
return out


def ndim(array) -> int:
# Required part of the duck array and the array-api, but we fall back in case
# https://docs.xarray.dev/en/latest/internals/duck-arrays-integration.html#duck-array-requirements
return array.ndim if hasattr(array, "ndim") else np.ndim(array)


def cumprod(array, axis=None, **kwargs):
"""N-dimensional version of cumprod."""
return _nd_cum_func(cumprod_1d, array, axis, **kwargs)
Expand Down
5 changes: 5 additions & 0 deletions xarray/core/extension_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ def __extension_duck_array__where(
return cast(T_ExtensionArray, pd.Series(x).where(condition, pd.Series(y)).array)


@implements(np.ndim)
def __extension_duck_array__ndim(x: PandasExtensionArray) -> int:
return x.ndim


@implements(np.reshape)
def __extension_duck_array__reshape(
arr: T_ExtensionArray, shape: tuple
Expand Down
5 changes: 3 additions & 2 deletions xarray/core/indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -769,14 +769,15 @@ def __repr__(self) -> str:

def _wrap_numpy_scalars(array):
"""Wrap NumPy scalars in 0d arrays."""
if np.ndim(array) == 0 and (
ndim = duck_array_ops.ndim(array)
if ndim == 0 and (
isinstance(array, np.generic)
or not (is_duck_array(array) or isinstance(array, NDArrayMixin))
):
return np.array(array)
elif hasattr(array, "dtype"):
return array
elif np.ndim(array) == 0:
elif ndim == 0:
return np.array(array)
else:
return array
Expand Down
5 changes: 4 additions & 1 deletion xarray/indexes/range_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import numpy as np
import pandas as pd

from xarray.core import duck_array_ops
from xarray.core.coordinate_transform import CoordinateTransform
from xarray.core.dataarray import DataArray
from xarray.core.indexes import CoordinateTransformIndex, Index, PandasIndex
Expand Down Expand Up @@ -320,7 +321,9 @@ def isel(

if isinstance(idxer, slice):
return RangeIndex(self.transform.slice(idxer))
elif (isinstance(idxer, Variable) and idxer.ndim > 1) or np.ndim(idxer) == 0:
elif (isinstance(idxer, Variable) and idxer.ndim > 1) or duck_array_ops.ndim(
idxer
) == 0:
return None
else:
values = self.transform.forward({self.dim: np.asarray(idxer)})[
Expand Down
18 changes: 15 additions & 3 deletions xarray/tests/test_variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from xarray import DataArray, Dataset, IndexVariable, Variable, set_options
from xarray.core import dtypes, duck_array_ops, indexing
from xarray.core.common import full_like, ones_like, zeros_like
from xarray.core.extension_array import PandasExtensionArray
from xarray.core.indexing import (
BasicIndexer,
CopyOnWriteArray,
Expand Down Expand Up @@ -2894,6 +2895,7 @@ class TestBackendIndexing:
@pytest.fixture(autouse=True)
def setUp(self):
self.d = np.random.random((10, 3)).astype(np.float64)
self.cat = PandasExtensionArray(pd.Categorical(["a", "b"] * 5))

def check_orthogonal_indexing(self, v):
assert np.allclose(v.isel(x=[8, 3], y=[2, 1]), self.d[[8, 3]][:, [2, 1]])
Expand All @@ -2913,6 +2915,14 @@ def test_NumpyIndexingAdapter(self):
dims=("x", "y"), data=NumpyIndexingAdapter(NumpyIndexingAdapter(self.d))
)

def test_extension_array_duck_array(self):
lazy = LazilyIndexedArray(self.cat)
assert (lazy.get_duck_array().array == self.cat).all()

def test_extension_array_duck_indexed(self):
lazy = Variable(dims=("x"), data=LazilyIndexedArray(self.cat))
assert (lazy[[0, 1, 5]] == ["a", "b", "b"]).all()

def test_LazilyIndexedArray(self):
v = Variable(dims=("x", "y"), data=LazilyIndexedArray(self.d))
self.check_orthogonal_indexing(v)
Expand Down Expand Up @@ -2951,12 +2961,14 @@ def test_MemoryCachedArray(self):
def test_DaskIndexingAdapter(self):
import dask.array as da

da = da.asarray(self.d)
v = Variable(dims=("x", "y"), data=DaskIndexingAdapter(da))
dask_array = da.asarray(self.d)
v = Variable(dims=("x", "y"), data=DaskIndexingAdapter(dask_array))
self.check_orthogonal_indexing(v)
self.check_vectorized_indexing(v)
# doubly wrapping
v = Variable(dims=("x", "y"), data=CopyOnWriteArray(DaskIndexingAdapter(da)))
v = Variable(
dims=("x", "y"), data=CopyOnWriteArray(DaskIndexingAdapter(dask_array))
)
self.check_orthogonal_indexing(v)
self.check_vectorized_indexing(v)

Expand Down
Loading