Skip to content

Commit 0b8fa41

Browse files
committed
Merge branch 'main' of https://github.com/pydata/xarray
2 parents 11170fc + 1219109 commit 0b8fa41

File tree

9 files changed

+390
-199
lines changed

9 files changed

+390
-199
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# xarray: N-D labeled arrays and datasets
22

3-
[![CI](https://github.com/pydata/xarray/workflows/CI/badge.svg?branch=main)](https://github.com/pydata/xarray/actions?query=workflow%3ACI)
3+
[![CI](https://github.com/pydata/xarray/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/pydata/xarray/actions/workflows/ci.yaml?query=branch%3Amain)
44
[![Code coverage](https://codecov.io/gh/pydata/xarray/branch/main/graph/badge.svg?flag=unittests)](https://codecov.io/gh/pydata/xarray)
55
[![Docs](https://readthedocs.org/projects/xray/badge/?version=latest)](https://docs.xarray.dev/)
66
[![Benchmarked with asv](https://img.shields.io/badge/benchmarked%20by-asv-green.svg?style=flat)](https://asv-runner.github.io/asv-collection/xarray/)

doc/whats-new.rst

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,47 @@ What's New
1414
1515
np.random.seed(123456)
1616
17-
.. _whats-new.2025.02.0:
17+
.. _whats-new.2025.03.1:
1818

19-
v2025.02.0 (unreleased)
19+
v2025.03.1 (unreleased)
2020
-----------------------
2121

22+
New Features
23+
~~~~~~~~~~~~
24+
25+
26+
Breaking changes
27+
~~~~~~~~~~~~~~~~
28+
29+
30+
Deprecations
31+
~~~~~~~~~~~~
32+
33+
34+
Bug fixes
35+
~~~~~~~~~
36+
37+
38+
Documentation
39+
~~~~~~~~~~~~~
40+
41+
42+
Internal Changes
43+
~~~~~~~~~~~~~~~~
44+
45+
.. _whats-new.2025.03.0:
46+
47+
v2025.03.0 (Mar 20, 2025)
48+
-------------------------
49+
50+
This release brings tested support for Python 3.13, support for reading Zarr V3 datasets into a :py:class:`~xarray.DataTree`,
51+
significant improvements to datetime & timedelta encoding/decoding, and improvements to the :py:class:`~xarray.DataTree` API;
52+
in addition to the usual bug fixes and other improvements.
53+
Thanks to the 26 contributors to this release:
54+
Alfonso Ladino, Benoit Bovy, Chuck Daniels, Deepak Cherian, Eni, Florian Jetter, Ian Hunt-Isaak, Jan, Joe Hamman, Josh Kihm, Julia Signell,
55+
Justus Magin, Kai Mühlbauer, Kobe Vandelanotte, Mathias Hauser, Max Jones, Maximilian Roos, Oliver Watt-Meyer, Sam Levang, Sander van Rijn,
56+
Spencer Clark, Stephan Hoyer, Tom Nicholas, Tom White, Vecko and maddogghoek
57+
2258
New Features
2359
~~~~~~~~~~~~
2460
- Added :py:meth:`tutorial.open_datatree` and :py:meth:`tutorial.load_datatree`
@@ -66,6 +102,10 @@ Deprecations
66102

67103
Bug fixes
68104
~~~~~~~~~
105+
106+
- Fix ``open_datatree`` incompatibilities with Zarr-Python V3 and refactor
107+
``TestZarrDatatreeIO`` accordingly (:issue:`9960`, :pull:`10020`).
108+
By `Alfonso Ladino-Rincon <https://github.com/aladinor>`_.
69109
- Default to resolution-dependent optimal integer encoding units when saving
70110
chunked non-nanosecond :py:class:`numpy.datetime64` or
71111
:py:class:`numpy.timedelta64` arrays to disk. Previously units of
@@ -75,8 +115,8 @@ Bug fixes
75115
- Use mean of min/max years as offset in calculation of datetime64 mean
76116
(:issue:`10019`, :pull:`10035`).
77117
By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
78-
- Fix DataArray().drop_attrs(deep=False) and add support for attrs to
79-
DataArray()._replace(). (:issue:`10027`, :pull:`10030`). By `Jan
118+
- Fix ``DataArray().drop_attrs(deep=False)`` and add support for attrs to
119+
``DataArray()._replace()``. (:issue:`10027`, :pull:`10030`). By `Jan
80120
Haacker <https://github.com/j-haacker>`_.
81121
- Fix bug preventing encoding times with missing values with small integer
82122
dtype (:issue:`9134`, :pull:`9498`). By `Spencer Clark
@@ -97,14 +137,12 @@ Bug fixes
97137
datetimes and timedeltas (:issue:`8957`, :pull:`10050`).
98138
By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
99139

140+
100141
Documentation
101142
~~~~~~~~~~~~~
102143
- Better expose the :py:class:`Coordinates` class in API reference (:pull:`10000`)
103144
By `Benoit Bovy <https://github.com/benbovy>`_.
104145

105-
Internal Changes
106-
~~~~~~~~~~~~~~~~
107-
108146

109147
.. _whats-new.2025.01.2:
110148

@@ -179,9 +217,6 @@ New Features
179217
:py:class:`pandas.DatetimeIndex` (:pull:`9965`). By `Spencer Clark
180218
<https://github.com/spencerkclark>`_ and `Kai Mühlbauer
181219
<https://github.com/kmuehlbauer>`_.
182-
183-
Breaking changes
184-
~~~~~~~~~~~~~~~~
185220
- Adds shards to the list of valid_encodings in the zarr backend, so that
186221
sharded Zarr V3s can be written (:issue:`9947`, :pull:`9948`).
187222
By `Jacob Prince_Bieker <https://github.com/jacobbieker>`_

xarray/backends/zarr.py

Lines changed: 56 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -666,10 +666,21 @@ def open_store(
666666
use_zarr_fill_value_as_mask=use_zarr_fill_value_as_mask,
667667
zarr_format=zarr_format,
668668
)
669+
670+
from zarr import Group
671+
672+
group_members: dict[str, Group] = {}
669673
group_paths = list(_iter_zarr_groups(zarr_group, parent=group))
670-
return {
674+
for path in group_paths:
675+
if path == group:
676+
group_members[path] = zarr_group
677+
else:
678+
rel_path = path.removeprefix(f"{group}/")
679+
group_members[path] = zarr_group[rel_path.removeprefix("/")]
680+
681+
out = {
671682
group: cls(
672-
zarr_group.get(group),
683+
group_store,
673684
mode,
674685
consolidate_on_close,
675686
append_dim,
@@ -680,8 +691,9 @@ def open_store(
680691
use_zarr_fill_value_as_mask,
681692
cache_members=cache_members,
682693
)
683-
for group in group_paths
694+
for group, group_store in group_members.items()
684695
}
696+
return out
685697

686698
@classmethod
687699
def open_group(
@@ -1034,8 +1046,6 @@ def store(
10341046
if self._consolidate_on_close:
10351047
kwargs = {}
10361048
if _zarr_v3():
1037-
# https://github.com/zarr-developers/zarr-python/pull/2113#issuecomment-2386718323
1038-
kwargs["path"] = self.zarr_group.name.lstrip("/")
10391049
kwargs["zarr_format"] = self.zarr_group.metadata.zarr_format
10401050
zarr.consolidate_metadata(self.zarr_group.store, **kwargs)
10411051

@@ -1662,8 +1672,6 @@ def open_groups_as_dict(
16621672
zarr_version=None,
16631673
zarr_format=None,
16641674
) -> dict[str, Dataset]:
1665-
from xarray.core.treenode import NodePath
1666-
16671675
filename_or_obj = _normalize_path(filename_or_obj)
16681676

16691677
# Check for a group and make it a parent if it exists
@@ -1686,7 +1694,6 @@ def open_groups_as_dict(
16861694
)
16871695

16881696
groups_dict = {}
1689-
16901697
for path_group, store in stores.items():
16911698
store_entrypoint = StoreBackendEntrypoint()
16921699

@@ -1762,44 +1769,57 @@ def _get_open_params(
17621769
consolidated = False
17631770

17641771
if _zarr_v3():
1765-
missing_exc = ValueError
1772+
# TODO: replace AssertionError after https://github.com/zarr-developers/zarr-python/issues/2821 is resolved
1773+
missing_exc = AssertionError
17661774
else:
17671775
missing_exc = zarr.errors.GroupNotFoundError
17681776

1769-
if consolidated is None:
1770-
try:
1771-
zarr_group = zarr.open_consolidated(store, **open_kwargs)
1772-
except (ValueError, KeyError):
1773-
# ValueError in zarr-python 3.x, KeyError in 2.x.
1777+
if consolidated in [None, True]:
1778+
# open the root of the store, in case there is metadata consolidated there
1779+
group = open_kwargs.pop("path")
1780+
1781+
if consolidated:
1782+
# TODO: an option to pass the metadata_key keyword
1783+
zarr_root_group = zarr.open_consolidated(store, **open_kwargs)
1784+
elif consolidated is None:
1785+
# same but with more error handling in case no consolidated metadata found
17741786
try:
1775-
zarr_group = zarr.open_group(store, **open_kwargs)
1776-
emit_user_level_warning(
1777-
"Failed to open Zarr store with consolidated metadata, "
1778-
"but successfully read with non-consolidated metadata. "
1779-
"This is typically much slower for opening a dataset. "
1780-
"To silence this warning, consider:\n"
1781-
"1. Consolidating metadata in this existing store with "
1782-
"zarr.consolidate_metadata().\n"
1783-
"2. Explicitly setting consolidated=False, to avoid trying "
1784-
"to read consolidate metadata, or\n"
1785-
"3. Explicitly setting consolidated=True, to raise an "
1786-
"error in this case instead of falling back to try "
1787-
"reading non-consolidated metadata.",
1788-
RuntimeWarning,
1789-
)
1790-
except missing_exc as err:
1791-
raise FileNotFoundError(
1792-
f"No such file or directory: '{store}'"
1793-
) from err
1794-
elif consolidated:
1795-
# TODO: an option to pass the metadata_key keyword
1796-
zarr_group = zarr.open_consolidated(store, **open_kwargs)
1787+
zarr_root_group = zarr.open_consolidated(store, **open_kwargs)
1788+
except (ValueError, KeyError):
1789+
# ValueError in zarr-python 3.x, KeyError in 2.x.
1790+
try:
1791+
zarr_root_group = zarr.open_group(store, **open_kwargs)
1792+
emit_user_level_warning(
1793+
"Failed to open Zarr store with consolidated metadata, "
1794+
"but successfully read with non-consolidated metadata. "
1795+
"This is typically much slower for opening a dataset. "
1796+
"To silence this warning, consider:\n"
1797+
"1. Consolidating metadata in this existing store with "
1798+
"zarr.consolidate_metadata().\n"
1799+
"2. Explicitly setting consolidated=False, to avoid trying "
1800+
"to read consolidate metadata, or\n"
1801+
"3. Explicitly setting consolidated=True, to raise an "
1802+
"error in this case instead of falling back to try "
1803+
"reading non-consolidated metadata.",
1804+
RuntimeWarning,
1805+
)
1806+
except missing_exc as err:
1807+
raise FileNotFoundError(
1808+
f"No such file or directory: '{store}'"
1809+
) from err
1810+
1811+
# but the user should still receive a DataTree whose root is the group they asked for
1812+
if group and group != "/":
1813+
zarr_group = zarr_root_group[group.removeprefix("/")]
1814+
else:
1815+
zarr_group = zarr_root_group
17971816
else:
17981817
if _zarr_v3():
17991818
# we have determined that we don't want to use consolidated metadata
18001819
# so we set that to False to avoid trying to read it
18011820
open_kwargs["use_consolidated"] = False
18021821
zarr_group = zarr.open_group(store, **open_kwargs)
1822+
18031823
close_store_on_close = zarr_group.store is not store
18041824

18051825
# we use this to determine how to handle fill_value

xarray/core/datatree.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
TYPE_CHECKING,
1717
Any,
1818
Concatenate,
19-
Literal,
2019
NoReturn,
2120
ParamSpec,
2221
TypeVar,
@@ -1741,7 +1740,7 @@ def to_zarr(
17411740
consolidated: bool = True,
17421741
group: str | None = None,
17431742
write_inherited_coords: bool = False,
1744-
compute: Literal[True] = True,
1743+
compute: bool = True,
17451744
**kwargs,
17461745
):
17471746
"""

xarray/tests/__init__.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import xarray.testing
1818
from xarray import Dataset
19+
from xarray.coding.times import _STANDARD_CALENDARS as _STANDARD_CALENDARS_UNSORTED
1920
from xarray.core.duck_array_ops import allclose_or_equiv # noqa: F401
2021
from xarray.core.extension_array import PandasExtensionArray
2122
from xarray.core.options import set_options
@@ -164,6 +165,21 @@ def _importorskip(
164165

165166
has_array_api_strict, requires_array_api_strict = _importorskip("array_api_strict")
166167

168+
parametrize_zarr_format = pytest.mark.parametrize(
169+
"zarr_format",
170+
[
171+
pytest.param(2, id="zarr_format=2"),
172+
pytest.param(
173+
3,
174+
marks=pytest.mark.skipif(
175+
not has_zarr_v3,
176+
reason="zarr-python v2 cannot understand the zarr v3 format",
177+
),
178+
id="zarr_format=3",
179+
),
180+
],
181+
)
182+
167183

168184
def _importorskip_h5netcdf_ros3(has_h5netcdf: bool):
169185
if not has_h5netcdf:
@@ -355,13 +371,36 @@ def create_test_data(
355371
return obj
356372

357373

358-
_CFTIME_CALENDARS = [
374+
_STANDARD_CALENDAR_NAMES = sorted(_STANDARD_CALENDARS_UNSORTED)
375+
_NON_STANDARD_CALENDAR_NAMES = {
376+
"noleap",
359377
"365_day",
360378
"360_day",
361379
"julian",
362380
"all_leap",
363381
"366_day",
364-
"gregorian",
365-
"proleptic_gregorian",
366-
"standard",
382+
}
383+
_NON_STANDARD_CALENDARS = [
384+
pytest.param(cal, marks=requires_cftime)
385+
for cal in sorted(_NON_STANDARD_CALENDAR_NAMES)
367386
]
387+
_STANDARD_CALENDARS = [pytest.param(cal) for cal in _STANDARD_CALENDAR_NAMES]
388+
_ALL_CALENDARS = sorted(_STANDARD_CALENDARS + _NON_STANDARD_CALENDARS)
389+
_CFTIME_CALENDARS = [
390+
pytest.param(*p.values, marks=requires_cftime) for p in _ALL_CALENDARS
391+
]
392+
393+
394+
def _all_cftime_date_types():
395+
import cftime
396+
397+
return {
398+
"noleap": cftime.DatetimeNoLeap,
399+
"365_day": cftime.DatetimeNoLeap,
400+
"360_day": cftime.Datetime360Day,
401+
"julian": cftime.DatetimeJulian,
402+
"all_leap": cftime.DatetimeAllLeap,
403+
"366_day": cftime.DatetimeAllLeap,
404+
"gregorian": cftime.DatetimeGregorian,
405+
"proleptic_gregorian": cftime.DatetimeProlepticGregorian,
406+
}

xarray/tests/test_accessor_dt.py

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import xarray as xr
88
from xarray.tests import (
9+
_CFTIME_CALENDARS,
10+
_all_cftime_date_types,
911
assert_allclose,
1012
assert_array_equal,
1113
assert_chunks_equal,
@@ -390,15 +392,6 @@ def test_dask_accessor_method(self, method, parameters) -> None:
390392
assert_equal(actual.compute(), expected.compute())
391393

392394

393-
_CFTIME_CALENDARS = [
394-
"365_day",
395-
"360_day",
396-
"julian",
397-
"all_leap",
398-
"366_day",
399-
"gregorian",
400-
"proleptic_gregorian",
401-
]
402395
_NT = 100
403396

404397

@@ -407,6 +400,13 @@ def calendar(request):
407400
return request.param
408401

409402

403+
@pytest.fixture()
404+
def cftime_date_type(calendar):
405+
if calendar == "standard":
406+
calendar = "proleptic_gregorian"
407+
return _all_cftime_date_types()[calendar]
408+
409+
410410
@pytest.fixture()
411411
def times(calendar):
412412
import cftime
@@ -573,13 +573,6 @@ def test_dask_field_access(times_3d, data, field) -> None:
573573
assert_equal(result.compute(), expected)
574574

575575

576-
@pytest.fixture()
577-
def cftime_date_type(calendar):
578-
from xarray.tests.test_coding_times import _all_cftime_date_types
579-
580-
return _all_cftime_date_types()[calendar]
581-
582-
583576
@requires_cftime
584577
def test_seasons(cftime_date_type) -> None:
585578
dates = xr.DataArray(

0 commit comments

Comments
 (0)