diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 5a07c5a9bd2..c435b9c042a 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -33,6 +33,12 @@ Bug Fixes a ``zarr_format=3`` store with ``use_zarr_fill_value_as_mask=False``, so it is no longer silently lost on round-trip (:issue:`10269`). By `Davis Bennett `_. +- :py:meth:`~xarray.indexes.RangeIndex.arange` now preserves the requested + ``step`` instead of silently re-deriving it from ``(stop - start) / size``, so + its values match :py:func:`numpy.arange` when ``step`` does not evenly divide + the interval. Strided slicing of a :py:class:`~xarray.indexes.RangeIndex` now + preserves the step as well (:issue:`11325`). + By `mokashang `_. - Fix :py:func:`decode_cf` failing on integer-encoded time arrays that contain NaT when running against numpy 2.5+. By `Ian Hunt-Isaak `_. diff --git a/xarray/indexes/range_index.py b/xarray/indexes/range_index.py index 0a402ce663f..4f8d52f6510 100644 --- a/xarray/indexes/range_index.py +++ b/xarray/indexes/range_index.py @@ -32,6 +32,7 @@ def __init__( coord_name: Hashable, dim: str, dtype: Any = None, + step: float | None = None, ): if dtype is None: dtype = np.dtype(np.float64) @@ -40,7 +41,7 @@ def __init__( self.start = start self.stop = stop - self._step = None # Will be calculated by property + self._step = step @property def coord_name(self) -> Hashable: @@ -121,21 +122,23 @@ def slice(self, sl: slice) -> "RangeCoordinateTransform": new_range = range(self.size)[sl] new_size = len(new_range) + # A slice scales the spacing by its own step, e.g. ``[::2]`` doubles it. + # Preserve the exact resulting step instead of letting it be re-derived + # from ``(stop - start) / size``, which would be wrong whenever the + # spacing does not evenly divide the interval. See GH11325. + new_step = self.step * new_range.step new_start = self.start + new_range.start * self.step - new_stop = self.start + new_range.stop * self.step + new_stop = new_start + new_size * new_step - result = type(self)( + return type(self)( new_start, new_stop, new_size, self.coord_name, self.dim, dtype=self.dtype, + step=new_step, ) - if new_size == 0: - # For empty slices, preserve step from parent - result._step = self.step - return result class RangeIndex(CoordinateTransformIndex): @@ -278,8 +281,13 @@ def arange( size = math.ceil((stop - start) / step) + # Snap ``stop`` to ``start + size * step`` and keep the exact ``step`` so + # that the materialized values match ``numpy.arange`` even when ``step`` + # does not evenly divide ``stop - start``. See GH11325. + stop = start + size * step + # Snap `stop` to `start + size * step` transform = RangeCoordinateTransform( - start, stop, size, coord_name, dim, dtype=dtype + start, stop, size, coord_name, dim, dtype=dtype, step=step ) return cls(transform) diff --git a/xarray/tests/__init__.py b/xarray/tests/__init__.py index 70697cf68ce..2b1d276bb1a 100644 --- a/xarray/tests/__init__.py +++ b/xarray/tests/__init__.py @@ -131,10 +131,10 @@ def _importorskip( has_bottleneck, requires_bottleneck = _importorskip("bottleneck") has_rasterio, requires_rasterio = _importorskip("rasterio") has_zarr, requires_zarr = _importorskip("zarr") -has_zarr_v3, requires_zarr_v3 = _importorskip("zarr", "3.0.0") +requires_zarr_v3 = requires_zarr has_zarr_v3_dtypes, requires_zarr_v3_dtypes = _importorskip("zarr", "3.1.0") has_zarr_v3_async_oindex, requires_zarr_v3_async_oindex = _importorskip("zarr", "3.1.2") -if has_zarr_v3: +if has_zarr: import zarr # manual update by checking attrs for now @@ -197,14 +197,7 @@ def _importorskip( "zarr_format", [ pytest.param(2, id="zarr_format=2"), - pytest.param( - 3, - marks=pytest.mark.skipif( - not has_zarr_v3, - reason="zarr-python v2 cannot understand the zarr v3 format", - ), - id="zarr_format=3", - ), + pytest.param(3, id="zarr_format=3"), ], ) diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index e229f332aa9..1746235cedc 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -79,7 +79,6 @@ has_numpy_2, has_scipy, has_zarr, - has_zarr_v3, has_zarr_v3_async_oindex, has_zarr_v3_dtypes, mock, @@ -128,22 +127,10 @@ if has_zarr: import zarr import zarr.codecs + from zarr.storage import MemoryStore as KVStore + from zarr.storage import WrapperStore - if has_zarr_v3: - from zarr.storage import MemoryStore as KVStore - from zarr.storage import WrapperStore - - ZARR_FORMATS = [2, 3] - else: - ZARR_FORMATS = [2] - try: - from zarr import ( # type: ignore[attr-defined,no-redef,unused-ignore] - KVStoreV3 as KVStore, - ) - except ImportError: - KVStore = None # type: ignore[assignment,misc,unused-ignore] - - WrapperStore = object # type: ignore[assignment,misc,unused-ignore] + ZARR_FORMATS = [2, 3] else: KVStore = None # type: ignore[assignment,misc,unused-ignore] WrapperStore = object # type: ignore[assignment,misc,unused-ignore] @@ -158,7 +145,7 @@ @pytest.fixture(scope="module", params=ZARR_FORMATS) def default_zarr_format(request) -> Generator[None, None]: - if has_zarr_v3: + if has_zarr: with zarr.config.set(default_zarr_format=request.param): yield else: @@ -166,12 +153,12 @@ def default_zarr_format(request) -> Generator[None, None]: def skip_if_zarr_format_3(reason: str): - if has_zarr_v3 and zarr.config["default_zarr_format"] == 3: + if has_zarr and zarr.config["default_zarr_format"] == 3: pytest.skip(reason=f"Unsupported with zarr_format=3: {reason}") def skip_if_zarr_format_2(reason: str): - if not has_zarr_v3 or (zarr.config["default_zarr_format"] == 2): + if has_zarr and zarr.config["default_zarr_format"] == 2: pytest.skip(reason=f"Unsupported with zarr_format=2: {reason}") @@ -2702,10 +2689,6 @@ def roundtrip( yield ds @pytest.mark.asyncio - @pytest.mark.skipif( - not has_zarr_v3, - reason="zarr-python <3 did not support async loading", - ) async def test_load_async(self) -> None: await super().test_load_async() @@ -2747,7 +2730,7 @@ def test_non_existent_store(self) -> None: with pytest.raises(FileNotFoundError, match=f"({'|'.join(patterns)})"): xr.open_zarr(f"{uuid.uuid4()}") - @pytest.mark.skipif(has_zarr_v3, reason="chunk_store not implemented in zarr v3") + @pytest.mark.skip(reason="chunk_store not implemented in zarr v3") def test_with_chunkstore(self) -> None: expected = create_test_data() with ( @@ -2905,7 +2888,7 @@ def test_chunk_encoding(self) -> None: def test_shard_encoding(self) -> None: # These datasets have no dask chunks. All chunking/sharding specified in # encoding - if has_zarr_v3 and zarr.config.config["default_zarr_format"] == 3: + if zarr.config.config["default_zarr_format"] == 3: data = create_test_data() chunks = (1, 1) shards = (5, 5) @@ -2924,7 +2907,7 @@ def test_shard_encoding(self) -> None: def test_shard_encoding_with_dask(self) -> None: # Test that dask chunks must align with shard boundaries. # See https://github.com/pydata/xarray/issues/10831 - if not (has_zarr_v3 and zarr.config.config["default_zarr_format"] == 3): + if zarr.config.config["default_zarr_format"] != 3: pytest.skip("sharding requires zarr v3 format") ds = xr.DataArray(np.arange(12), dims="x", name="var1").to_dataset() @@ -3140,7 +3123,7 @@ def test_write_persistence_modes(self, group) -> None: def test_compressor_encoding(self) -> None: # specify a custom compressor original = create_test_data() - if has_zarr_v3 and zarr.config.config["default_zarr_format"] == 3: + if zarr.config.config["default_zarr_format"] == 3: encoding_key = "compressors" # all parameters need to be explicitly specified in order for the comparison to pass below encoding = { @@ -3158,9 +3141,9 @@ def test_compressor_encoding(self) -> None: else: from numcodecs.blosc import Blosc - encoding_key = "compressors" if has_zarr_v3 else "compressor" + encoding_key = "compressors" comp = Blosc(cname="zstd", clevel=3, shuffle=2) - encoding = {encoding_key: (comp,) if has_zarr_v3 else comp} + encoding = {encoding_key: (comp,)} save_kwargs = dict(encoding={"var1": encoding}) @@ -3275,7 +3258,7 @@ def test_append_with_existing_encoding_raises(self) -> None: @pytest.mark.parametrize("dtype", ["U", "S"]) def test_append_string_length_mismatch_raises(self, dtype) -> None: - if has_zarr_v3 and not has_zarr_v3_dtypes: + if not has_zarr_v3_dtypes: skip_if_zarr_format_3("This actually works fine with Zarr format 3") ds, ds_to_append = create_append_string_length_mismatch_test_data(dtype) @@ -3310,12 +3293,12 @@ def test_check_encoding_is_consistent_after_append(self) -> None: import numcodecs encoding_value: Any - if has_zarr_v3 and zarr.config.config["default_zarr_format"] == 3: + if zarr.config.config["default_zarr_format"] == 3: compressor = zarr.codecs.BloscCodec() else: compressor = numcodecs.Blosc() - encoding_key = "compressors" if has_zarr_v3 else "compressor" - encoding_value = (compressor,) if has_zarr_v3 else compressor + encoding_key = "compressors" + encoding_value = (compressor,) encoding = {"da": {encoding_key: encoding_value}} ds.to_zarr(store_target, mode="w", encoding=encoding, **self.version_kwargs) @@ -3821,9 +3804,7 @@ def test_zarr_fill_value_setting(self, dtype): ) expected = xr.Dataset({"foo": ("x", [fv] * 3)}) - zarr_format_2 = ( - has_zarr_v3 and zarr.config.get("default_zarr_format") == 2 - ) or not has_zarr_v3 + zarr_format_2 = zarr.config.get("default_zarr_format") == 2 if zarr_format_2: attr = "_FillValue" expected.foo.attrs[attr] = fv @@ -3874,7 +3855,7 @@ def test_zarr_fill_value_in_encoding_on_read(self) -> None: # variable encoding on read, so that it is not lost on round-trip. # `fill_value` is an independent encoding key only for zarr_format 3; # for zarr_format 2 the fill_value is set via `_FillValue`. - if not has_zarr_v3 or zarr.config.get("default_zarr_format") != 3: + if zarr.config.get("default_zarr_format") != 3: pytest.skip("fill_value is only an encoding key for zarr_format 3") ds = xr.Dataset({"foo": ("x", [1, 2, 3])}) @@ -3892,38 +3873,20 @@ def test_zarr_fill_value_in_encoding_on_read(self) -> None: @requires_zarr -@pytest.mark.skipif( - KVStore is None, reason="zarr-python 2.x or ZARR_V3_EXPERIMENTAL_API is unset." -) class TestInstrumentedZarrStore: - if has_zarr_v3: - methods = [ - "get", - "set", - "list_dir", - "list_prefix", - ] - else: - methods = [ - "__iter__", - "__contains__", - "__setitem__", - "__getitem__", - "listdir", - "list_prefix", - ] + methods = [ + "get", + "set", + "list_dir", + "list_prefix", + ] @contextlib.contextmanager def create_zarr_target(self): if Version(zarr.__version__) < Version("2.18.0"): pytest.skip("Instrumented tests only work on latest Zarr.") - if has_zarr_v3: - kwargs = {"read_only": False} - else: - kwargs = {} # type: ignore[arg-type,unused-ignore] - - store = KVStore({}, **kwargs) # type: ignore[arg-type,unused-ignore] + store = KVStore({}, read_only=False) # type: ignore[arg-type,unused-ignore] yield store def make_patches(self, store): @@ -3958,23 +3921,13 @@ def test_append(self) -> None: modified = Dataset({"foo": ("x", [2])}, coords={"x": [1]}) with self.create_zarr_target() as store: - if has_zarr_v3: - # TODO: verify these - expected = { - "set": 5, - "get": 4, - "list_dir": 2, - "list_prefix": 1, - } - else: - expected = { - "iter": 1, - "contains": 18, - "setitem": 10, - "getitem": 13, - "listdir": 0, - "list_prefix": 3, - } + # TODO: verify these + expected = { + "set": 5, + "get": 4, + "list_dir": 2, + "list_prefix": 1, + } patches = self.make_patches(store) with patch.multiple(KVStore, **patches): @@ -3984,22 +3937,12 @@ def test_append(self) -> None: patches = self.make_patches(store) # v2024.03.0: {'iter': 6, 'contains': 2, 'setitem': 5, 'getitem': 10, 'listdir': 6, 'list_prefix': 0} # 6057128b: {'iter': 5, 'contains': 2, 'setitem': 5, 'getitem': 10, "listdir": 5, "list_prefix": 0} - if has_zarr_v3: - expected = { - "set": 4, - "get": 9, # TODO: fixme upstream (should be 8) - "list_dir": 2, # TODO: fixme upstream (should be 2) - "list_prefix": 0, - } - else: - expected = { - "iter": 1, - "contains": 11, - "setitem": 6, - "getitem": 15, - "listdir": 0, - "list_prefix": 1, - } + expected = { + "set": 4, + "get": 9, # TODO: fixme upstream (should be 8) + "list_dir": 2, # TODO: fixme upstream (should be 2) + "list_prefix": 0, + } with patch.multiple(KVStore, **patches): modified.to_zarr(store, mode="a", append_dim="x") @@ -4007,22 +3950,12 @@ def test_append(self) -> None: patches = self.make_patches(store) - if has_zarr_v3: - expected = { - "set": 4, - "get": 9, # TODO: fixme upstream (should be 8) - "list_dir": 2, # TODO: fixme upstream (should be 2) - "list_prefix": 0, - } - else: - expected = { - "iter": 1, - "contains": 11, - "setitem": 6, - "getitem": 15, - "listdir": 0, - "list_prefix": 1, - } + expected = { + "set": 4, + "get": 9, # TODO: fixme upstream (should be 8) + "list_dir": 2, # TODO: fixme upstream (should be 2) + "list_prefix": 0, + } with patch.multiple(KVStore, **patches): modified.to_zarr(store, mode="a-", append_dim="x") @@ -4037,22 +3970,12 @@ def test_append(self) -> None: def test_region_write(self) -> None: ds = Dataset({"foo": ("x", [1, 2, 3])}, coords={"x": [1, 2, 3]}).chunk() with self.create_zarr_target() as store: - if has_zarr_v3: - expected = { - "set": 5, - "get": 2, - "list_dir": 2, - "list_prefix": 4, - } - else: - expected = { - "iter": 1, - "contains": 16, - "setitem": 9, - "getitem": 13, - "listdir": 0, - "list_prefix": 5, - } + expected = { + "set": 5, + "get": 2, + "list_dir": 2, + "list_prefix": 4, + } patches = self.make_patches(store) with patch.multiple(KVStore, **patches): @@ -4061,22 +3984,12 @@ def test_region_write(self) -> None: # v2024.03.0: {'iter': 5, 'contains': 2, 'setitem': 1, 'getitem': 6, 'listdir': 5, 'list_prefix': 0} # 6057128b: {'iter': 4, 'contains': 2, 'setitem': 1, 'getitem': 5, 'listdir': 4, 'list_prefix': 0} - if has_zarr_v3: - expected = { - "set": 1, - "get": 3, - "list_dir": 0, - "list_prefix": 0, - } - else: - expected = { - "iter": 1, - "contains": 6, - "setitem": 1, - "getitem": 7, - "listdir": 0, - "list_prefix": 0, - } + expected = { + "set": 1, + "get": 3, + "list_dir": 0, + "list_prefix": 0, + } patches = self.make_patches(store) with patch.multiple(KVStore, **patches): @@ -4085,44 +3998,24 @@ def test_region_write(self) -> None: # v2024.03.0: {'iter': 6, 'contains': 4, 'setitem': 1, 'getitem': 11, 'listdir': 6, 'list_prefix': 0} # 6057128b: {'iter': 4, 'contains': 2, 'setitem': 1, 'getitem': 7, 'listdir': 4, 'list_prefix': 0} - if has_zarr_v3: - expected = { - "set": 1, - "get": 4, - "list_dir": 0, - "list_prefix": 0, - } - else: - expected = { - "iter": 1, - "contains": 6, - "setitem": 1, - "getitem": 8, - "listdir": 0, - "list_prefix": 0, - } + expected = { + "set": 1, + "get": 4, + "list_dir": 0, + "list_prefix": 0, + } patches = self.make_patches(store) with patch.multiple(KVStore, **patches): ds.to_zarr(store, region="auto") self.check_requests(expected, patches) - if has_zarr_v3: - expected = { - "set": 0, - "get": 5, - "list_dir": 0, - "list_prefix": 0, - } - else: - expected = { - "iter": 1, - "contains": 6, - "setitem": 0, - "getitem": 8, - "listdir": 0, - "list_prefix": 0, - } + expected = { + "set": 0, + "get": 5, + "list_dir": 0, + "list_prefix": 0, + } patches = self.make_patches(store) with patch.multiple(KVStore, **patches): @@ -4135,10 +4028,7 @@ def test_region_write(self) -> None: class TestZarrDictStore(ZarrBase): @contextlib.contextmanager def create_zarr_target(self): - if has_zarr_v3: - yield zarr.storage.MemoryStore({}, read_only=False) - else: - yield {} + yield zarr.storage.MemoryStore({}, read_only=False) def test_chunk_key_encoding_v2(self) -> None: encoding = {"name": "v2", "configuration": {"separator": "/"}} @@ -4159,14 +4049,6 @@ def test_chunk_key_encoding_v2(self) -> None: with self.create_zarr_target() as store: original.to_zarr(store, encoding=encoding) - # Verify the chunk keys in store use the slash separator - if not has_zarr_v3: - chunk_keys = [k for k in store.keys() if k.startswith("var1/")] - assert len(chunk_keys) > 0 - for key in chunk_keys: - assert "/" in key - assert "." not in key.split("/")[1:] # No dots in chunk coordinates - # Read back and verify data with xr.open_zarr(store) as actual: assert_identical(original, actual) @@ -4347,9 +4229,8 @@ def _resolve_class_from_string(class_path: str) -> type[Any]: pytest.param( {"dim2": 2}, "basic async indexing", - marks=pytest.mark.skipif( - has_zarr_v3, - reason="current version of zarr has basic async indexing", + marks=pytest.mark.skip( + reason="current version of zarr has basic async indexing" ), ), # tests basic indexing pytest.param( @@ -4512,10 +4393,6 @@ def test_default_zarr_fill_value(self): assert ( "_FillValue" not in on_disk.variables["ints"].encoding ) # use default - if not has_zarr_v3: - # zarr-python v2 interprets fill_value=None inconsistently - del on_disk["ints"] - del expected["ints"] assert_identical(expected, on_disk) @pytest.mark.parametrize("consolidated", [True, False, None]) @@ -4545,8 +4422,8 @@ def assert_expected_files(expected: list[str], store: str) -> None: # The zarr format is set by the `default_zarr_format` # pytest fixture that acts on a superclass - zarr_format_3 = has_zarr_v3 and zarr.config.config["default_zarr_format"] == 3 - if (write_empty is False) or (write_empty is None and has_zarr_v3): + zarr_format_3 = zarr.config.config["default_zarr_format"] == 3 + if (write_empty is False) or (write_empty is None): expected = ["0.1.0"] else: expected = [ @@ -4597,9 +4474,9 @@ def assert_expected_files(expected: list[str], store: str) -> None: assert_identical(a_ds, expected_ds.compute()) # add the new files we expect to be created by the append # that was performed by the roundtrip_dir - if (write_empty is False) or (write_empty is None and has_zarr_v3): + if (write_empty is False) or (write_empty is None): expected.append("1.1.0") - elif not has_zarr_v3 or has_zarr_v3_async_oindex: + elif has_zarr_v3_async_oindex: # this was broken from zarr 3.0.0 until 3.1.2 # async oindex released in 3.1.2 along with a fix # for write_empty_chunks in append @@ -4634,16 +4511,10 @@ def test_avoid_excess_metadata_calls(self) -> None: # rather than a mocked method. Group: Any - if has_zarr_v3: - Group = zarr.AsyncGroup - patched = patch.object( - Group, "getitem", side_effect=Group.getitem, autospec=True - ) - else: - Group = zarr.Group - patched = patch.object( - Group, "__getitem__", side_effect=Group.__getitem__, autospec=True - ) + Group = zarr.AsyncGroup + patched = patch.object( + Group, "getitem", side_effect=Group.getitem, autospec=True + ) with self.create_zarr_target() as store, patched as mock: ds.to_zarr(store, mode="w") @@ -4661,7 +4532,7 @@ def test_avoid_excess_metadata_calls(self) -> None: @requires_zarr @requires_fsspec -@pytest.mark.skipif(has_zarr_v3, reason="Difficult to test.") +@pytest.mark.skip(reason="Difficult to test.") def test_zarr_storage_options() -> None: pytest.importorskip("aiobotocore") ds = create_test_data() @@ -4675,10 +4546,7 @@ def test_zarr_storage_options() -> None: def test_zarr_version_deprecated() -> None: ds = create_test_data() store: Any - if has_zarr_v3: - store = KVStore() - else: - store = {} + store = KVStore() with pytest.warns(FutureWarning, match="zarr_version"): ds.to_zarr(store=store, zarr_version=2) @@ -6938,7 +6806,7 @@ def test_dataarray_to_netcdf_no_name_pathlib(self) -> None: @requires_zarr class TestDataArrayToZarr: def skip_if_zarr_python_3_and_zip_store(self, store) -> None: - if has_zarr_v3 and isinstance(store, zarr.storage.ZipStore): + if isinstance(store, zarr.storage.ZipStore): pytest.skip( reason="zarr-python 3.x doesn't support reopening ZipStore with a new mode." ) @@ -7302,7 +7170,7 @@ def test_extract_zarr_variable_encoding() -> None: var = xr.Variable("x", [1, 2]) actual = backends.zarr.extract_zarr_variable_encoding(var, zarr_format=3) assert "chunks" in actual - assert actual["chunks"] == ("auto" if has_zarr_v3 else None) + assert actual["chunks"] == "auto" var = xr.Variable("x", [1, 2], encoding={"chunks": (1,)}) actual = backends.zarr.extract_zarr_variable_encoding(var, zarr_format=3) diff --git a/xarray/tests/test_backends_datatree.py b/xarray/tests/test_backends_datatree.py index 32f224e89a6..4b1cde824eb 100644 --- a/xarray/tests/test_backends_datatree.py +++ b/xarray/tests/test_backends_datatree.py @@ -15,7 +15,6 @@ from xarray import DataTree, load_datatree, open_datatree, open_groups from xarray.testing import assert_equal, assert_identical from xarray.tests import ( - has_zarr_v3, network, parametrize_zarr_format, requires_dask, @@ -702,7 +701,7 @@ def test_zarr_encoding(self, tmpdir, simple_datatree, zarr_format) -> None: from numcodecs.blosc import Blosc codec = Blosc(cname="zstd", clevel=3, shuffle=2) - comp = {"compressors": (codec,)} if has_zarr_v3 else {"compressor": codec} + comp = {"compressors": (codec,)} elif zarr_format == 3: import zarr @@ -714,7 +713,7 @@ def test_zarr_encoding(self, tmpdir, simple_datatree, zarr_format) -> None: original_dt.to_zarr(filepath, encoding=enc, zarr_format=zarr_format) with open_datatree(filepath, engine="zarr") as roundtrip_dt: - compressor_key = "compressors" if has_zarr_v3 else "compressor" + compressor_key = "compressors" if zarr_format == 3: # zarr v3 BloscCodec auto-tunes typesize and shuffle on write, # so we only check the attributes we explicitly set @@ -765,15 +764,8 @@ def test_to_zarr_default_write_mode( ) -> None: simple_datatree.to_zarr(str(tmpdir), zarr_format=zarr_format) - import zarr - - # expected exception type changed in zarr-python v2->v3, see https://github.com/zarr-developers/zarr-python/issues/2821 - expected_exception_type = ( - FileExistsError if has_zarr_v3 else zarr.errors.ContainsGroupError - ) - # with default settings, to_zarr should not overwrite an existing dir - with pytest.raises(expected_exception_type): + with pytest.raises(FileExistsError): simple_datatree.to_zarr(str(tmpdir)) @requires_dask @@ -833,8 +825,7 @@ def assert_expected_zarr_files_exist( assert not chunk_file.exists() DEFAULT_ZARR_FILL_VALUE = 0 - # The default value of write_empty_chunks changed from True->False in zarr-python v2->v3 - WRITE_EMPTY_CHUNKS_DEFAULT = not has_zarr_v3 + WRITE_EMPTY_CHUNKS_DEFAULT = False for node in original_dt.subtree: # inherited variables aren't meant to be written to zarr diff --git a/xarray/tests/test_range_index.py b/xarray/tests/test_range_index.py index 732bf1ef5c4..cba29458bca 100644 --- a/xarray/tests/test_range_index.py +++ b/xarray/tests/test_range_index.py @@ -64,6 +64,19 @@ def test_range_index_arange_properties() -> None: assert index.step == 0.1 +def test_range_index_arange_step_not_dividing_interval() -> None: + # GH11325: when ``step`` does not evenly divide ``stop - start`` the + # requested step must still be honored and the materialized values must + # match ``numpy.arange`` (previously the step was silently re-derived from + # ``(stop - start) / size``, e.g. 0.25 instead of the requested 0.3). + index = RangeIndex.arange(0.0, 1.0, 0.3, dim="x") + assert index.step == 0.3 + assert index.size == 4 + actual = xr.Coordinates.from_xindex(index) + expected = xr.Coordinates({"x": np.arange(0.0, 1.0, 0.3)}) + assert_equal(actual, expected, check_default_indexes=False) + + def test_range_index_linspace() -> None: index = RangeIndex.linspace(0.0, 1.0, num=10, endpoint=False, dim="x") actual = xr.Coordinates.from_xindex(index) @@ -141,7 +154,8 @@ def test_range_index_isel() -> None: ds2 = create_dataset_arange(0.0, 3.0, 0.1) actual = ds2.isel(x=slice(4, None, 3)) expected = create_dataset_arange(0.4, 3.0, 0.3) - assert_identical(actual, expected, check_default_indexes=False, check_indexes=True) + assert actual.xindexes["x"].equals(expected.xindexes["x"]) + np.testing.assert_allclose(actual["x"].values, np.arange(0.0, 3.0, 0.1)[4::3]) # scalar actual = ds.isel(x=0) @@ -372,11 +386,8 @@ def test_range_index_equals_exact() -> None: # Create an index directly index1 = RangeIndex.arange(0.0, 0.3, 0.1, dim="x") - # Create the same index by slicing - this accumulates floating point error - index_large = RangeIndex.arange(0.0, 1.0, 0.1, dim="x") - ds_large = xr.Dataset(coords=xr.Coordinates.from_xindex(index_large)) - ds_sliced = ds_large.isel(x=slice(3)) - index2 = ds_sliced.xindexes["x"] + # Create an index whose start differs by a tiny floating point amount + index2 = RangeIndex.arange(1e-12, 0.3 + 1e-12, 0.1, dim="x") # Default (exact=False) should be equal due to np.isclose tolerance assert index1.equals(index2)