From 1161f7b42507b54f01660f850fdf4922676b018a Mon Sep 17 00:00:00 2001 From: Anirban Mandal Date: Tue, 29 Sep 2026 11:49:07 +0530 Subject: [PATCH 1/3] Document how to find unlimited_dims via encoding (#11618) * Document how to check unlimited_dims in encoding * [pre-commit.ci] auto fixes from pre-commit.com hooks * Fix unlimited_dims example and formatting * Set unlimited_dims when writing example file * Add whats-new entry for unlimited_dims docs --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- doc/user-guide/io.md | 15 ++++++++++++++- doc/whats-new.rst | 5 +++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/doc/user-guide/io.md b/doc/user-guide/io.md index 839a76935f1..3a9aa5155d6 100644 --- a/doc/user-guide/io.md +++ b/doc/user-guide/io.md @@ -358,7 +358,7 @@ ds = xr.Dataset( }, ) -ds.to_netcdf(nc_filename) +ds.to_netcdf(nc_filename, unlimited_dims=["y"]) ``` By default, the file is saved as netCDF4 (assuming netCDF4-Python is @@ -510,6 +510,19 @@ ds_disk["y"].encoding ds_disk.encoding ``` +One key piece of information you can find in the encoding is whether a +dimension is unlimited. Unlimited dimensions can grow in size, and are +commonly used for a dimension like `time` that is appended to over +time: + +```{code-cell} +ds_disk.encoding["unlimited_dims"] +``` + +This information can also be set explicitly when writing a file with +{py:meth}`Dataset.to_netcdf` using the `unlimited_dims` keyword +argument. + Note that all operations that manipulate variables other than indexing will remove encoding information. diff --git a/doc/whats-new.rst b/doc/whats-new.rst index d03dee7ce42..2fdeef5197d 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -172,6 +172,11 @@ Documentation - Migrated from nbsphinx/jupyter-execute to myst-nb (:issue:`7924`, :pull:`11456`). By `Nick Hodgskin `_. +- Added an example to the netCDF section of the IO user guide showing how to + check which dimensions are unlimited via ``Dataset.encoding`` + (:issue:`7517`, :pull:`11618`). + By `Anirban Mandal `_. + Internal Changes ~~~~~~~~~~~~~~~~ From 154bdf69f33edc99929f8c6987fc566587a3a109 Mon Sep 17 00:00:00 2001 From: Kropiunig <48442031+Kropiunig@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:40:10 +0200 Subject: [PATCH 2/3] Fix str.replace(..., n=0) replacing every occurrence instead of none (#11545) Co-authored-by: Deepak Cherian --- doc/whats-new.rst | 5 +++++ xarray/core/accessor_str.py | 6 ++++-- xarray/tests/test_accessor_str.py | 22 ++++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 2fdeef5197d..b3056edd5a6 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -53,6 +53,11 @@ Deprecations Bug Fixes ~~~~~~~~~ +- Fixed ``DataArray.str.replace`` replacing every occurrence instead of none when + ``n=0``. ``re.sub`` treats ``count=0`` as "replace all", so the regex code path + collapsed ``n=0`` onto ``n=-1``, while the ``regex=False`` path already handled + ``n=0`` correctly (:pull:`11545`). + By `Alexander Kropiunig `_. - ``min`` and ``max`` of object arrays with ``skipna=False`` now return ``NaN`` for slices containing missing values, instead of a result that depended on the position of the missing value (:issue:`11501`, :pull:`#11627`). diff --git a/xarray/core/accessor_str.py b/xarray/core/accessor_str.py index db92c14d4fe..872c552ec82 100644 --- a/xarray/core/accessor_str.py +++ b/xarray/core/accessor_str.py @@ -1944,8 +1944,10 @@ def replace( if regex: pat = self._re_compile(pat=pat, flags=flags, case=case) - func = lambda x, ipat, irepl, i_n: ipat.sub( - repl=irepl, string=x, count=max(i_n, 0) + # ``re.sub`` interprets ``count=0`` as "replace every occurrence", + # so ``n=0`` has to be special-cased to mean "replace nothing". + func = lambda x, ipat, irepl, i_n: ( + x if i_n == 0 else ipat.sub(repl=irepl, string=x, count=max(i_n, 0)) ) else: pat = self._stringify(pat) diff --git a/xarray/tests/test_accessor_str.py b/xarray/tests/test_accessor_str.py index 0741fa364fc..5f5849bc9de 100644 --- a/xarray/tests/test_accessor_str.py +++ b/xarray/tests/test_accessor_str.py @@ -381,6 +381,28 @@ def test_replace(dtype) -> None: assert_equal(result, expected) +def test_replace_n_zero(dtype) -> None: + # ``n=0`` means "make no replacements", for regex and literal patterns alike + values = xr.DataArray(["fooBAD__barBAD"], dims=["x"]).astype(dtype) + + result = values.str.replace("BAD[_]*", "", n=0) + assert result.dtype == values.dtype + assert_equal(result, values) + + result = values.str.replace("BAD", "", n=0, regex=False) + assert result.dtype == values.dtype + assert_equal(result, values) + + # ``n`` is broadcast, so a single zero must not spill over to its neighbours + n = xr.DataArray([0, 1, -1], dims=["y"]) + result = values.str.replace("BAD[_]*", "", n=n) + expected = xr.DataArray( + [["fooBAD__barBAD", "foobarBAD", "foobar"]], dims=["x", "y"] + ).astype(dtype) + assert result.dtype == expected.dtype + assert_equal(result, expected) + + def test_replace_callable() -> None: values = xr.DataArray(["fooBAD__barBAD"]) From ede38e513f648eaf38fc5acf5ad03fba05c35d8d Mon Sep 17 00:00:00 2001 From: Asish Kumar <87874775+officialasishkumar@users.noreply.github.com> Date: Tue, 29 Sep 2026 13:17:13 +0530 Subject: [PATCH 3/3] Warn when explicit dims rename tuple-style DataArray coords (#11292) * Warn when tuple-style DataArray coords are renamed --------- Signed-off-by: Asish Kumar Co-authored-by: Michael Niklas --- doc/whats-new.rst | 3 +++ xarray/core/dataarray.py | 15 +++++++++++++++ xarray/tests/test_dataarray.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index b3056edd5a6..864b344c4ab 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -438,6 +438,9 @@ Bug Fixes By `Emmanuel Ferdman `_. - :func:`combine_by_coords` no longer returns an empty dataset when a generator is passed as ``data_objects`` (:issue:`10114`, :pull:`11265`). By `Amartya Anand `_. +- Warn when tuple-style ``DataArray`` coordinates are renamed by explicitly + provided dimension names (:issue:`11234`, :pull:`11292`). + By `Asish Kumar `_. - Preserve non-grouped coordinates in fallback ``groupby`` reductions when grouping by a non-leading dimension reorders the underlying variable dimensions (:issue:`11188`, :pull:`11290`). diff --git a/xarray/core/dataarray.py b/xarray/core/dataarray.py index 8d201ae1a21..01fa20a14bd 100644 --- a/xarray/core/dataarray.py +++ b/xarray/core/dataarray.py @@ -167,6 +167,21 @@ def _infer_coords_and_dims( if not hashable(d): raise TypeError(f"Dimension {d} is not hashable") + if coords is not None and not utils.is_dict_like(coords): + if any( + isinstance(coord, tuple) + and len(coord) >= 2 + and hashable(coord[0]) + and coord[0] != dim + for dim, coord in zip(dims_tuple, coords, strict=True) + ): + utils.emit_user_level_warning( + "Coordinate names in tuple-style coords are ignored when `dims` " + "are provided. Use a mapping for `coords` if you need named " + "coordinates.", + UserWarning, + ) + new_coords: Mapping[Hashable, Any] if isinstance(coords, Coordinates): diff --git a/xarray/tests/test_dataarray.py b/xarray/tests/test_dataarray.py index 805b96b9160..3b441825e84 100644 --- a/xarray/tests/test_dataarray.py +++ b/xarray/tests/test_dataarray.py @@ -410,6 +410,38 @@ def test_constructor(self) -> None: expected = DataArray([1, 2, 3], coords=[("x", [0, 1, 2])]) assert_identical(expected, actual) + def test_constructor_tuple_coords_warn_when_dims_override_names(self) -> None: + data = np.random.random((2, 3)) + coords = [("a", [0, 1]), ("b", [-1, -2, -3])] + + with pytest.warns( + UserWarning, + match="Coordinate names in tuple-style coords are ignored", + ): + actual = DataArray(data, coords=coords, dims=["x", "y"]) + + expected = Dataset( + {None: (["x", "y"], data)}, + coords={"x": [0, 1], "y": [-1, -2, -3]}, + )[None] + assert_identical(expected, actual) + + def test_constructor_tuple_coords_no_warning_when_names_match_dims(self) -> None: + data = np.random.random((2, 3)) + + with assert_no_warnings(): + actual = DataArray( + data, + coords=[("x", [0, 1]), ("y", [-1, -2, -3])], + dims=["x", "y"], + ) + + expected = Dataset( + {None: (["x", "y"], data)}, + coords={"x": [0, 1], "y": [-1, -2, -3]}, + )[None] + assert_identical(expected, actual) + def test_constructor_invalid(self) -> None: data = np.random.randn(3, 2)