Skip to content

Commit 2105aa0

Browse files
committed
Add legacy backend indexing tests
1 parent dcd3ac9 commit 2105aa0

File tree

7 files changed

+131
-53
lines changed

7 files changed

+131
-53
lines changed

xarray/backends/common.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,12 @@ def robust_getitem(array, key, catch=Exception, max_retries=6, initial_delay=500
250250

251251

252252
class BackendArray(NdimSizeLenMixin, indexing.ExplicitlyIndexed):
253+
def get_duck_array(self, dtype: np.typing.DTypeLike = None):
254+
key = indexing.BasicIndexer((slice(None),) * self.ndim)
255+
return self[key] # type: ignore [index]
256+
257+
258+
class NewBackendArray(NdimSizeLenMixin, indexing.ExplicitlyIndexed):
253259
__slots__ = ("indexing_support",)
254260

255261
def get_duck_array(self, dtype: np.typing.DTypeLike = None):

xarray/backends/netCDF4_.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
from xarray import coding
1313
from xarray.backends.common import (
1414
BACKEND_ENTRYPOINTS,
15-
BackendArray,
1615
BackendEntrypoint,
16+
NewBackendArray,
1717
WritableCFDataStore,
1818
_normalize_path,
1919
datatree_from_dict_with_io_cleanup,
@@ -61,7 +61,7 @@
6161
NETCDF4_PYTHON_LOCK = combine_locks([NETCDFC_LOCK, HDF5_LOCK])
6262

6363

64-
class BaseNetCDF4Array(BackendArray):
64+
class BaseNetCDF4Array(NewBackendArray):
6565
__slots__ = ("datastore", "dtype", "shape", "variable_name")
6666

6767
def __init__(self, variable_name, datastore):

xarray/backends/pydap_.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from xarray.backends.common import (
99
BACKEND_ENTRYPOINTS,
1010
AbstractDataStore,
11-
BackendArray,
1211
BackendEntrypoint,
12+
NewBackendArray,
1313
robust_getitem,
1414
)
1515
from xarray.backends.store import StoreBackendEntrypoint
@@ -36,7 +36,7 @@
3636
)
3737

3838

39-
class PydapArrayWrapper(BackendArray):
39+
class PydapArrayWrapper(NewBackendArray):
4040
indexing_support = indexing.IndexingSupport.BASIC
4141

4242
def __init__(self, array) -> None:

xarray/backends/scipy_.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010

1111
from xarray.backends.common import (
1212
BACKEND_ENTRYPOINTS,
13-
BackendArray,
1413
BackendEntrypoint,
14+
NewBackendArray,
1515
WritableCFDataStore,
1616
_normalize_path,
1717
)
@@ -59,7 +59,7 @@ def _decode_attrs(d):
5959
return {k: v if k == "_FillValue" else _decode_string(v) for (k, v) in d.items()}
6060

6161

62-
class ScipyArrayWrapper(BackendArray):
62+
class ScipyArrayWrapper(NewBackendArray):
6363
indexing_support = indexing.IndexingSupport.OUTER_1VECTOR
6464

6565
def __init__(self, variable_name, datastore):

xarray/backends/zarr.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
from xarray.backends.common import (
1515
BACKEND_ENTRYPOINTS,
1616
AbstractWritableDataStore,
17-
BackendArray,
1817
BackendEntrypoint,
18+
NewBackendArray,
1919
_encode_variable_name,
2020
_normalize_path,
2121
datatree_from_dict_with_io_cleanup,
@@ -185,7 +185,7 @@ def encode_zarr_attr_value(value):
185185
return encoded
186186

187187

188-
class ZarrArrayWrapper(BackendArray):
188+
class ZarrArrayWrapper(NewBackendArray):
189189
indexing_support = indexing.IndexingSupport.VECTORIZED
190190

191191
def __init__(self, zarr_array):

xarray/core/indexing.py

Lines changed: 32 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import enum
44
import functools
55
import operator
6-
import warnings
76
from collections import Counter, defaultdict
87
from collections.abc import Callable, Hashable, Iterable, Mapping
98
from contextlib import suppress
@@ -23,6 +22,7 @@
2322
from xarray.core.utils import (
2423
NDArrayMixin,
2524
either_dict_or_kwargs,
25+
emit_user_level_warning,
2626
get_valid_numpy_dtype,
2727
is_duck_array,
2828
is_duck_dask_array,
@@ -609,7 +609,7 @@ def __getitem__(self, key) -> Any:
609609
BackendArray_fallback_warning_message = (
610610
"The array `{0}` does not support indexing using the .vindex and .oindex properties. "
611611
"The __getitem__ method is being used instead. This fallback behavior will be "
612-
"removed in a future version. Please ensure that the backend array `{1}` implements "
612+
"removed in a future version. Please ensure that the backend array `{0}` implements "
613613
"support for the .vindex and .oindex properties to avoid potential issues."
614614
)
615615

@@ -671,21 +671,8 @@ def shape(self) -> _Shape:
671671
return self._shape
672672

673673
def get_duck_array(self) -> Any:
674-
try:
675-
array = apply_indexer(self.array, self.key)
676-
except NotImplementedError as _:
677-
# If the array is not an ExplicitlyIndexedNDArrayMixin,
678-
# it may wrap a BackendArray subclass that doesn't implement .oindex and .vindex. so use its __getitem__
679-
warnings.warn(
680-
BackendArray_fallback_warning_message.format(
681-
self.array.__class__.__name__, self.array.__class__.__name__
682-
),
683-
category=DeprecationWarning,
684-
stacklevel=2,
685-
)
686-
array = self.array[self.key]
687-
688-
# self.array[self.key] is now a numpy array when
674+
array = apply_indexer(self.array, self.key)
675+
# array[self.key] is now a numpy array when
689676
# self.array is a BackendArray subclass
690677
# and self.key is BasicIndexer((slice(None, None, None),))
691678
# so we need the explicit check for ExplicitlyIndexed
@@ -752,21 +739,9 @@ def shape(self) -> _Shape:
752739
return np.broadcast(*self.key.tuple).shape
753740

754741
def get_duck_array(self) -> Any:
755-
try:
756-
array = apply_indexer(self.array, self.key)
757-
except NotImplementedError as _:
758-
# If the array is not an ExplicitlyIndexedNDArrayMixin,
759-
# it may wrap a BackendArray subclass that doesn't implement .oindex and .vindex. so use its __getitem__
760-
warnings.warn(
761-
BackendArray_fallback_warning_message.format(
762-
self.array.__class__.__name__, self.array.__class__.__name__
763-
),
764-
category=PendingDeprecationWarning,
765-
stacklevel=2,
766-
)
767-
array = self.array[self.key]
742+
array = apply_indexer(self.array, self.key)
768743

769-
# self.array[self.key] is now a numpy array when
744+
# array is now a numpy array when
770745
# self.array is a BackendArray subclass
771746
# and self.key is BasicIndexer((slice(None, None, None),))
772747
# so we need the explicit check for ExplicitlyIndexed
@@ -1136,6 +1111,7 @@ def vectorized_indexing_adapter(
11361111
)
11371112

11381113

1114+
# TODO: deprecate and delete this method once it is no longer used externally
11391115
def explicit_indexing_adapter(
11401116
key: ExplicitIndexer,
11411117
shape: _Shape,
@@ -1163,26 +1139,36 @@ def explicit_indexing_adapter(
11631139
-------
11641140
Indexing result, in the form of a duck numpy-array.
11651141
"""
1166-
# TODO: raise PendingDeprecationWarning here.
1167-
if isinstance(key, VectorizedIndexer):
1168-
return vectorized_indexing_adapter(
1169-
key.tuple, shape, indexing_support, raw_indexing_method
1170-
)
1171-
elif isinstance(key, OuterIndexer):
1172-
return outer_indexing_adapter(
1173-
key.tuple, shape, indexing_support, raw_indexing_method
1174-
)
1175-
elif isinstance(key, BasicIndexer):
1176-
return basic_indexing_adapter(
1177-
key.tuple, shape, indexing_support, raw_indexing_method
1178-
)
1179-
raise TypeError(f"unexpected key type: {key}")
1142+
1143+
# If the array is not an ExplicitlyIndexedNDArrayMixin,
1144+
# it may wrap a BackendArray subclass that doesn't implement .oindex and .vindex. so use its __getitem__
1145+
emit_user_level_warning(
1146+
BackendArray_fallback_warning_message.format(""),
1147+
category=PendingDeprecationWarning,
1148+
)
1149+
raw_key, numpy_indices = decompose_indexer(key, shape, indexing_support)
1150+
result = raw_indexing_method(raw_key.tuple)
1151+
if numpy_indices.tuple:
1152+
indexable = NumpyIndexingAdapter(result)
1153+
result = apply_indexer(indexable, numpy_indices)
1154+
return result
11801155

11811156

11821157
def apply_indexer(
11831158
indexable: ExplicitlyIndexedNDArrayMixin, indexer: ExplicitIndexer
11841159
) -> Any:
11851160
"""Apply an indexer to an indexable object."""
1161+
if not hasattr(indexable, "vindex") and not hasattr(indexable, "oindex"):
1162+
# This path is used by Lazily*IndexedArray.get_duck_array()
1163+
classname = type(indexable).__name__
1164+
# If the array is not an ExplicitlyIndexedNDArrayMixin,
1165+
# it may wrap a BackendArray subclass that doesn't implement .oindex and .vindex. so use its __getitem__
1166+
emit_user_level_warning(
1167+
BackendArray_fallback_warning_message.format(classname),
1168+
category=PendingDeprecationWarning,
1169+
)
1170+
return indexable[indexer]
1171+
11861172
if isinstance(indexer, VectorizedIndexer):
11871173
return indexable.vindex[indexer.tuple]
11881174
elif isinstance(indexer, OuterIndexer):
@@ -1206,6 +1192,7 @@ def set_with_indexer(indexable, indexer: ExplicitIndexer, value: Any) -> None:
12061192
indexable[indexer.tuple] = value
12071193

12081194

1195+
# TODO: delete this method once explicit_indexing_adapter is no longer used externally
12091196
def decompose_indexer(
12101197
indexer: ExplicitIndexer, shape: _Shape, indexing_support: IndexingSupport
12111198
) -> tuple[ExplicitIndexer, ExplicitIndexer]:

xarray/tests/test_backends.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
open_mfdataset,
4040
save_mfdataset,
4141
)
42+
from xarray.backends.common import BackendArray as LegacyBackendArray
4243
from xarray.backends.common import robust_getitem
4344
from xarray.backends.h5netcdf_ import H5netcdfBackendEntrypoint
4445
from xarray.backends.netcdf3 import _nc3_dtype_coercions
@@ -53,6 +54,7 @@
5354
from xarray.coding.variables import SerializationWarning
5455
from xarray.conventions import encode_dataset_coordinates
5556
from xarray.core import indexing
57+
from xarray.core.indexing import IndexingSupport
5658
from xarray.core.options import set_options
5759
from xarray.core.utils import module_available
5860
from xarray.namedarray.pycompat import array_type
@@ -352,6 +354,9 @@ def test_dtype_coercion_error(self) -> None:
352354

353355

354356
class BackendIndexingTestsMixin:
357+
def roundtrip(self, ds: Dataset, open_kwargs=None) -> Dataset:
358+
raise NotImplementedError
359+
355360
def test_orthogonal_indexing(self) -> None:
356361
in_memory = create_test_data()
357362
with self.roundtrip(in_memory) as on_disk:
@@ -6491,3 +6496,83 @@ def test_zarr_safe_chunk_region(tmp_path):
64916496
chunk = ds.isel(region)
64926497
chunk = chunk.chunk()
64936498
chunk.chunk().to_zarr(store, region=region)
6499+
6500+
6501+
class LegacyBackendArrayWrapper(LegacyBackendArray):
6502+
def __init__(self, array: np.ndarray, indexing_support: IndexingSupport):
6503+
self.shape = array.shape
6504+
self.dtype = array.dtype
6505+
self.array = array
6506+
self.indexing_support = indexing_support
6507+
6508+
def __getitem__(self, key: indexing.ExplicitIndexer):
6509+
return indexing.explicit_indexing_adapter(
6510+
key, self.shape, self.indexing_support, self._getitem
6511+
)
6512+
6513+
def _getitem(self, key: tuple[Any, ...]) -> np.ndarray:
6514+
return self.array[key]
6515+
6516+
6517+
def indexing_tests(*, indexing_support: IndexingSupport):
6518+
def wrapper(cls):
6519+
class NewClass(cls):
6520+
cls.indexing_support = indexing_support
6521+
6522+
def roundtrip(self, ds: Dataset, *, open_kwargs=None) -> Dataset:
6523+
ds = ds.copy(deep=True)
6524+
for name in list(ds.data_vars) + list(
6525+
set(ds.coords) - set(ds.xindexes)
6526+
):
6527+
var = ds._variables[name]
6528+
ds._variables[name] = var.copy(
6529+
# These tests assume that indexing is lazy (checks ._in_memory),
6530+
# so wrapping by LazilyIndexedArray is required.
6531+
data=indexing.LazilyIndexedArray(
6532+
LegacyBackendArrayWrapper(var.data, self.indexing_support)
6533+
)
6534+
)
6535+
return ds
6536+
6537+
def test_vectorized_indexing_negative_step(self) -> None:
6538+
with pytest.warns(PendingDeprecationWarning):
6539+
super().test_vectorized_indexing_negative_step()
6540+
6541+
def test_isel_dataarray(self) -> None:
6542+
with pytest.warns(PendingDeprecationWarning):
6543+
super().test_isel_dataarray()
6544+
6545+
def test_vectorized_indexing(self) -> None:
6546+
with pytest.warns(PendingDeprecationWarning):
6547+
super().test_vectorized_indexing()
6548+
6549+
def test_orthogonal_indexing(self) -> None:
6550+
with pytest.warns(PendingDeprecationWarning):
6551+
super().test_orthogonal_indexing()
6552+
6553+
def test_outer_indexing_reversed(self) -> None:
6554+
with pytest.warns(PendingDeprecationWarning):
6555+
super().test_outer_indexing_reversed()
6556+
6557+
return NewClass
6558+
6559+
return wrapper
6560+
6561+
6562+
@indexing_tests(indexing_support=IndexingSupport.BASIC)
6563+
class TestBasicIndexingLegacyBackend(BackendIndexingTestsMixin):
6564+
pass
6565+
6566+
6567+
@indexing_tests(indexing_support=IndexingSupport.OUTER_1VECTOR)
6568+
class TestOuter1VectorIndexingLegacyBackend(BackendIndexingTestsMixin):
6569+
pass
6570+
6571+
6572+
# @indexing_tests(indexing_support=IndexingSupport.OUTER)
6573+
# class TestOuterIndexingLegacyBackend(BackendIndexingTestsMixin):
6574+
# pass
6575+
6576+
# @indexing_tests(indexing_support=IndexingSupport.VECTORIZED)
6577+
# class TestVectorizedIndexingLegacyBackend(BackendIndexingTestsMixin):
6578+
# pass

0 commit comments

Comments
 (0)