diff --git a/xarray/coding/times.py b/xarray/coding/times.py index e66adc0887c..d24a2036dc3 100644 --- a/xarray/coding/times.py +++ b/xarray/coding/times.py @@ -28,7 +28,7 @@ from xarray.core.utils import attempt_import, emit_user_level_warning from xarray.core.variable import Variable from xarray.namedarray.parallelcompat import T_ChunkedArray, get_chunked_array_type -from xarray.namedarray.pycompat import is_chunked_array +from xarray.namedarray.pycompat import is_chunked_array, to_numpy from xarray.namedarray.utils import is_duck_dask_array try: @@ -310,7 +310,7 @@ def _decode_cf_datetime_dtype( # Dataset.__repr__ when users try to view their lazily decoded array. values = indexing.ImplicitToExplicitIndexingAdapter(indexing.as_indexable(data)) example_value = np.concatenate( - [first_n_items(values, 1) or [0], last_item(values) or [0]] + [to_numpy(first_n_items(values, 1)), to_numpy(last_item(values))] ) try: @@ -516,7 +516,7 @@ def decode_cf_datetime( -------- cftime.num2date """ - num_dates = np.asarray(num_dates) + num_dates = to_numpy(num_dates) flat_num_dates = ravel(num_dates) if calendar is None: calendar = "standard" @@ -643,7 +643,7 @@ def decode_cf_timedelta( """Given an array of numeric timedeltas in netCDF format, convert it into a numpy timedelta64 ["s", "ms", "us", "ns"] array. """ - num_timedeltas = np.asarray(num_timedeltas) + num_timedeltas = to_numpy(num_timedeltas) unit = _netcdf_to_numpy_timeunit(units) with warnings.catch_warnings(): diff --git a/xarray/core/formatting.py b/xarray/core/formatting.py index a6bacccbeef..993cddf2b57 100644 --- a/xarray/core/formatting.py +++ b/xarray/core/formatting.py @@ -18,12 +18,12 @@ from pandas.errors import OutOfBoundsDatetime from xarray.core.datatree_render import RenderDataTree -from xarray.core.duck_array_ops import array_all, array_any, array_equiv, astype +from xarray.core.duck_array_ops import array_all, array_any, array_equiv, astype, ravel from xarray.core.indexing import MemoryCachedArray from xarray.core.options import OPTIONS, _get_boolean_with_default from xarray.core.treenode import group_subtrees from xarray.core.utils import is_duck_array -from xarray.namedarray.pycompat import array_type, to_duck_array, to_numpy +from xarray.namedarray.pycompat import array_type, to_duck_array if TYPE_CHECKING: from xarray.core.coordinates import AbstractCoordinates @@ -94,7 +94,7 @@ def first_n_items(array, n_desired): # pass Variable._data if isinstance(array, Variable): array = array._data - return np.ravel(to_duck_array(array))[:n_desired] + return ravel(to_duck_array(array))[:n_desired] def last_n_items(array, n_desired): @@ -118,18 +118,13 @@ def last_n_items(array, n_desired): # pass Variable._data if isinstance(array, Variable): array = array._data - return np.ravel(to_duck_array(array))[-n_desired:] + return ravel(to_duck_array(array))[-n_desired:] def last_item(array): - """Returns the last item of an array in a list or an empty list.""" - if array.size == 0: - # work around for https://github.com/numpy/numpy/issues/5195 - return [] - + """Returns the last item of an array.""" indexer = (slice(-1, None),) * array.ndim - # to_numpy since dask doesn't support tolist - return np.ravel(to_numpy(array[indexer])).tolist() + return ravel(to_duck_array(array[indexer])) def calc_max_rows_first(max_rows: int) -> int: diff --git a/xarray/namedarray/pycompat.py b/xarray/namedarray/pycompat.py index 3ce33d4d8ea..68b6a7853bf 100644 --- a/xarray/namedarray/pycompat.py +++ b/xarray/namedarray/pycompat.py @@ -102,6 +102,12 @@ def to_numpy( from xarray.core.indexing import ExplicitlyIndexed from xarray.namedarray.parallelcompat import get_chunked_array_type + try: + # for tests only at the moment + return data.to_numpy() # type: ignore[no-any-return,union-attr] + except AttributeError: + pass + if isinstance(data, ExplicitlyIndexed): data = data.get_duck_array() # type: ignore[no-untyped-call] @@ -122,7 +128,10 @@ def to_numpy( def to_duck_array(data: Any, **kwargs: dict[str, Any]) -> duckarray[_ShapeType, _DType]: - from xarray.core.indexing import ExplicitlyIndexed + from xarray.core.indexing import ( + ExplicitlyIndexed, + ImplicitToExplicitIndexingAdapter, + ) from xarray.namedarray.parallelcompat import get_chunked_array_type if is_chunked_array(data): @@ -130,7 +139,7 @@ def to_duck_array(data: Any, **kwargs: dict[str, Any]) -> duckarray[_ShapeType, loaded_data, *_ = chunkmanager.compute(data, **kwargs) # type: ignore[var-annotated] return loaded_data - if isinstance(data, ExplicitlyIndexed): + if isinstance(data, ExplicitlyIndexed | ImplicitToExplicitIndexingAdapter): return data.get_duck_array() # type: ignore[no-untyped-call, no-any-return] elif is_duck_array(data): return data diff --git a/xarray/tests/arrays.py b/xarray/tests/arrays.py index 7373b6c75ab..cc4c480c437 100644 --- a/xarray/tests/arrays.py +++ b/xarray/tests/arrays.py @@ -51,6 +51,10 @@ def __init__(self, array: np.ndarray): def __getitem__(self, key): return type(self)(self.array[key]) + def to_numpy(self) -> np.ndarray: + """Allow explicit conversions to numpy in `to_numpy`, but disallow np.asarray etc.""" + return self.array + def __array__( self, dtype: np.typing.DTypeLike = None, /, *, copy: bool | None = None ) -> np.ndarray: @@ -58,6 +62,9 @@ def __array__( def __array_namespace__(self): """Present to satisfy is_duck_array test.""" + from xarray.tests import namespace + + return namespace CONCATENATABLEARRAY_HANDLED_ARRAY_FUNCTIONS: dict[str, Callable] = {} diff --git a/xarray/tests/namespace.py b/xarray/tests/namespace.py new file mode 100644 index 00000000000..f0cc28f4b57 --- /dev/null +++ b/xarray/tests/namespace.py @@ -0,0 +1,5 @@ +from xarray.core import duck_array_ops + + +def reshape(array, shape, **kwargs): + return type(array)(duck_array_ops.reshape(array.array, shape=shape, **kwargs)) diff --git a/xarray/tests/test_coding_times.py b/xarray/tests/test_coding_times.py index e736339da1b..09408e32d08 100644 --- a/xarray/tests/test_coding_times.py +++ b/xarray/tests/test_coding_times.py @@ -41,6 +41,7 @@ from xarray.core.utils import is_duck_dask_array from xarray.testing import assert_equal, assert_identical from xarray.tests import ( + DuckArrayWrapper, FirstElementAccessibleArray, arm_xfail, assert_array_equal, @@ -1935,6 +1936,31 @@ def test_lazy_decode_timedelta_error() -> None: decoded.load() +@pytest.mark.parametrize( + "calendar", + [ + "standard", + pytest.param( + "360_day", marks=pytest.mark.skipif(not has_cftime, reason="no cftime") + ), + ], +) +def test_duck_array_decode_times(calendar) -> None: + from xarray.core.indexing import LazilyIndexedArray + + days = LazilyIndexedArray(DuckArrayWrapper(np.array([1.0, 2.0, 3.0]))) + var = Variable( + ["time"], days, {"units": "days since 2001-01-01", "calendar": calendar} + ) + decoded = conventions.decode_cf_variable( + "foo", var, decode_times=CFDatetimeCoder(use_cftime=None) + ) + if calendar not in _STANDARD_CALENDARS: + assert decoded.dtype == np.dtype("O") + else: + assert decoded.dtype == np.dtype("=M8[ns]") + + @pytest.mark.parametrize("decode_timedelta", [True, False]) @pytest.mark.parametrize("mask_and_scale", [True, False]) def test_decode_timedelta_mask_and_scale(
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: