Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 101 additions & 24 deletions .claude/skills/upgrade-min-versions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ This must pass. If it fails, there may be dependency conflicts to resolve.
pixi run policy-min-versions
```

All packages should now show `=`.
All packages should now show `=`, and the command must exit with 0. Only a `>`
(pin newer than the policy) fails the check; it is allowed for packages listed
under `ignored_violations` in `ci/policy.yaml`.

Remove `overrides` in `ci/policy.yaml` that are no longer needed.

### 6. Clean Up Obsolete Test Decorators

Expand All @@ -93,47 +97,120 @@ rg "has_scipy_ge_|has_dask_ge_|has_zarr_v3[^_]|has_pandas_ge_|has_numpy_2|has_da
For each file found:

- Remove imports of deleted decorators
- Remove `@requires_*` decorators that are always true
- Remove `if has_*:` conditionals (keep only the true branch)
- Replace `@requires_*` version decorators with the plain `@requires_<package>`
decorator, unless the test or its class already has one. They also skip the
test when the package is not installed at all, e.g. `@requires_dask_ge_2024_11_0`
in the `bare-minimum` and `no-dask` environments.
- Remove `if has_*:` conditionals (keep only the true branch), but use
`if has_<package>:` instead if the test doesn't require the package already.
- Remove `if not has_*:` conditionals (delete the code block)
- Delete tests marked with `@pytest.mark.skipif(has_*, ...)` (always skipped)

### 7. Update whats-new.rst
Also grep for decorators matching the new minimum exactly (e.g. `has_zarr_v3_dtypes`
for zarr 3.1) and for version guards defined elsewhere (e.g. `HAS_STRING_DTYPE`
in `xarray/compat/npcompat.py`).

### 7. Clean Up Version Guards in the Source

The source code has version guards too, which become dead code with the new
minimums. Search for them, not only for `module_available(..., minversion=...)`:

```bash
rg -n -i "minversion|__version__|mod_version|NumpyVersion|oldest supported|remove (once|when)|delete when|requires (numpy|dask|pandas|zarr)|numpy ?[<>]=? ?[0-9]" xarray --glob '!xarray/tests/**'
```

Also go through TODO and compat comments mentioning a dependency version, in
the source and the tests (e.g. `# use np.cumulative_sum(...) when we support
numpy>=2`, `# TODO: delete when min dask>=2023.12.1`, or `try: ... except
ImportError:` fallbacks for old versions):

```bash
rg -n -i "(TODO|FIXME|NOTE|compat|workaround|legacy|once|when|until|older|drop)[^\n]{0,80}\b(numpy|pandas|scipy|dask|zarr|netcdf4|h5netcdf|flox|numbagg|bottleneck|sparse|pint|matplotlib|cftime|numba)\b[^\n]{0,40}([<>]=?|version|v?[0-9]+\.[0-9]+)" xarray --glob '!xarray/core/_aggregations.py'
```

- Remove branches for versions older than the new minimum and keep the code for
the new versions, e.g. `try: from numpy.exceptions import RankWarning` /
`except ImportError:` becomes a plain import at the top of the module.
- `module_available("flox", minversion=...)` also checks that the package is
installed, so replace it with `module_available("flox")`, not `True`.
- Guards for versions _above_ the new minimum stay (e.g. `flox>=0.10.5`).
- Leave TODOs that depend on more than the version (e.g. a behavior change or
refactor) for a separate PR, and mention them in the PR.
- Delete compat modules that only re-export something available in all
supported versions, and their entries in `pyproject.toml` (e.g. coverage
`omit`).
- The flox checks in `xarray/core/_aggregations.py` are generated: change
`min_flox_version` in `xarray/util/generate_aggregations.py` and run
`pixi run generate-aggregations`. The generator writes to
`../xarray/xarray/...` relative to the current directory, so in a worktree
that is not named `xarray` it overwrites the main checkout instead. Run it
from a checkout named `xarray`, or check `git status` in both afterwards.

### 8. Update whats-new.rst

Add a table to `doc/whats-new.rst` under "Breaking Changes" documenting the version changes (in alphabetical order):

```rst
Breaking Changes
~~~~~~~~~~~~~~~~

- The minimum versions of some dependencies were changed:

===================== ========= =======
Package Old New
===================== ========= =======
boto3 1.34 1.36
cartopy 0.23 0.24
dask 2024.6 2025.1
distributed 2024.6 2025.1
h5netcdf 1.4 1.5
iris 3.9 3.11
lxml 5.1 5.3
matplotlib 3.8 3.10
numpy 1.26 2.0
packaging 24.1 24.2
rasterio 1.3 1.4
scipy 1.13 1.15
toolz 0.12 1.0
zarr 2.18 3.0
===================== ========= =======
- The minimum versions of some dependencies were changed (:pull:`<PR number>`):

=========== ====== ======
Package Old New
=========== ====== ======
boto3 1.34 1.36
cartopy 0.23 0.24
dask 2024.6 2025.1
distributed 2024.6 2025.1
h5netcdf 1.4 1.5
iris 3.9 3.11
lxml 5.1 5.3
matplotlib 3.8 3.10
numpy 1.26 2.0
packaging 24.1 24.2
rasterio 1.3 1.4
scipy 1.13 1.15
toolz 0.12 1.0
zarr 2.18 3.0
=========== ====== ======
```

Only edit this entry, older releases contain the same sentence.

Every value must lie within the `=` signs of its column, otherwise the docs
build fails with "Malformed table". Size the columns to the longest value, e.g.
`2025.9` for dask.

### 9. Open the PR

Always ask the user before opening the PR, and show them the description first.

Follow `.github/PULL_REQUEST_TEMPLATE.md`: a short description (an intro line
and a few one-line bullets, e.g. the notable bumps and any policy override),
the checklist with the relevant items ticked (whats-new) and the AI disclosure
section filled in. Then add the PR number to the whats-new entry.

## Common Issues

### Lock file conflicts

If `pixi lock` fails, check for incompatible version combinations. Some packages (like h5py/hdf5) are noted in comments as prone to conflicts.

A package that is not bumped by the policy can still conflict with the new
minimums, e.g. netcdf4 1.6 has no builds for numpy 2. Bump it to the oldest
version that solves, and make sure the policy check still passes.

### Test failures after cleanup

After removing version guards, some tests may fail if they relied on version-specific behavior. Update the test logic to use only the new minimum version's behavior.

Run the full test suite in an environment with the latest versions (e.g.
`test-py314`) as well, not only in `test-py312-min-versions`: a workaround whose
TODO names an old version may still be needed for newer versions.

Run the touched tests not only in `test-py312-min-versions` but also in
`test-py312-bare-minimum` and `test-py314-no-dask`, which catch removed
decorators that also skipped tests for missing packages. Run mypy in the CI
environment, `pixi run -e test-py314-with-typing -- python -m mypy`; the
`typing` environment lacks optional dependencies and reports false positives.
6 changes: 5 additions & 1 deletion ci/policy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ policy:
numpy: 18
default: 12
# overrides for the policy
overrides: {}
overrides:
# conda-forge has no netcdf4 1.7.0 (the series starts at 1.7.1), which the
# policy needs to consider 1.7 released. Remove once
# https://github.com/xarray-contrib/minimum-dependency-versions/pull/94 is released
netcdf4: "1.7"
# these packages are completely ignored
exclude:
- coveralls
Expand Down
25 changes: 25 additions & 0 deletions doc/whats-new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,31 @@ Breaking Changes
- Support for Python 3.11 has been dropped. The minimum required Python version
is now 3.12, in line with xarray's
:ref:`minimum dependency policy <mindeps_policy>` (:pull:`11649`).
By `Michael Niklas <https://github.com/headtr1ck>`_.
- The minimum versions of some dependencies were changed (:pull:`11728`):

=========== ====== ======
Package Old New
=========== ====== ======
boto3 1.37 1.40
bottleneck 1.4 1.6
cartopy 0.24 0.25
dask 2025.2 2025.9
distributed 2025.2 2025.9
h5py 3.13 3.14
iris 3.11 3.13
lxml 5.3 6.0
netcdf4 1.6 1.7
numba 0.61 0.62
numpy 1.26 2.2
packaging 24.2 25.0
pandas 2.2 2.3
pint 0.24 0.25
scipy 1.15 1.16
sparse 0.15 0.17
zarr 3.0 3.1
=========== ====== ======

By `Michael Niklas <https://github.com/headtr1ck>`_.
- :py:class:`~xarray.NamedArray` is now generic in the type of its dimension
names, so static type checkers can infer and check them. It takes three type
Expand Down
36 changes: 18 additions & 18 deletions pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ python = "*"
numpy = "*"
pandas = "*"

packaging = "24.2.*"
packaging = "25.0.*"
git = "*" # needed for dynamic versioning

[dependencies]
Expand Down Expand Up @@ -101,46 +101,46 @@ jax = "*" # no way to get cpu-only jaxlib from conda if gpu is present
[feature.minimal.dependencies]
# minimal versions
python = "3.12.*"
numpy = "1.26.*"
pandas = "2.2.*"
numpy = "2.2.*"
pandas = "2.3.*"

[feature.minimum-scipy.dependencies]
scipy = "1.15.*"
scipy = "1.16.*"

[feature.min-versions.dependencies]
array-api-strict = "2.4.*" # dependency for testing the array api compat
boto3 = "1.37.*"
bottleneck = "1.4.*"
cartopy = "0.24.*"
boto3 = "1.40.*"
bottleneck = "1.6.*"
cartopy = "0.25.*"
cftime = "1.6.*"
dask-core = "2025.2.*"
distributed = "2025.2.*"
dask-core = "2025.9.*"
distributed = "2025.9.*"
flox = "0.10.*"
# h5netcdf 1.8.0 introduced a few compatibility features with netcdf4
# https://github.com/pydata/xarray/issues/10657#issuecomment-3711095986
h5netcdf = "1.8.*"
# h5py and hdf5 tend to cause conflicts
# for e.g. hdf5 1.12 conflicts with h5py=3.1
# prioritize bumping other packages instead
h5py = "3.13.*"
h5py = "3.14.*"
hdf5 = "1.14.*"
iris = "3.11.*"
lxml = "5.3.*" # Optional dep of pydap
iris = "3.13.*"
lxml = "6.0.*" # Optional dep of pydap
matplotlib-base = "3.10.*"
nc-time-axis = "1.4.*"
# netcdf follows a 1.major.minor[.patch] convention
# (see https://github.com/Unidata/netcdf4-python/issues/1090)
netcdf4 = "1.6.*"
numba = "0.61.*"
netcdf4 = "1.7.*"
numba = "0.62.*"
numbagg = "0.9.*"
packaging = "24.2.*"
pint = "0.24.*"
packaging = "25.0.*"
pint = "0.25.*"
pydap = "3.5.*"
rasterio = "1.4.*"
seaborn = "0.13.*"
sparse = "0.15.*"
sparse = "0.17.*"
toolz = "1.0.*"
zarr = "3.0.*"
zarr = "3.1.*"
pyarrow = "*" # required by dask.dataframe
# numcodecs warns on import when crc32c is used instead of google-crc32c
google-crc32c = "*"
Expand Down
15 changes: 7 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ name = "xarray"
readme = "README.md"
requires-python = ">=3.12"

dependencies = ["numpy>=1.26", "packaging>=24.2", "pandas>=2.2"]
dependencies = ["numpy>=2.2", "packaging>=25.0", "pandas>=2.3"]

# We don't encode minimum requirements here (though if we can write a script to
# generate the text from `min_deps_check.py`, that's welcome...). We do add
Expand All @@ -27,7 +27,7 @@ dependencies = ["numpy>=1.26", "packaging>=24.2", "pandas>=2.2"]

[project.optional-dependencies]
accel = [
"scipy>=1.15",
"scipy>=1.16",
"bottleneck",
"numbagg>=0.9,!=0.9.5", # 0.9.5 breaks dask.array.nanmedian
"numba>=0.62", # numba 0.62 added support for numpy 2.3
Expand All @@ -36,21 +36,21 @@ accel = [
]
complete = ["xarray[accel,etc,io,parallel,viz]"]
io = [
"netCDF4>=1.6.0",
"netCDF4>=1.7.0",
# h5netcdf 1.8.0 introduced a few compatibility features with netcdf4
# https://github.com/pydata/xarray/issues/10657#issuecomment-3711095986
"h5netcdf[h5py]>=1.8.0",
"pydap",
"scipy>=1.15",
"zarr>=3.0",
"scipy>=1.16",
"zarr>=3.1",
"fsspec",
"cftime",
"pooch",
]
arrow = ["pyarrow"]
etc = ["sparse>=0.15"]
etc = ["sparse>=0.17"]
parallel = ["dask[complete]"]
viz = ["cartopy>=0.24", "matplotlib>=3.10", "nc-time-axis", "seaborn"]
viz = ["cartopy>=0.25", "matplotlib>=3.10", "nc-time-axis", "seaborn"]
types = [
"pandas-stubs",
"scipy-stubs",
Expand Down Expand Up @@ -118,7 +118,6 @@ fallback_version = "9999"
omit = [
"*/xarray/tests/*",
"*/xarray/compat/dask_array_compat.py",
"*/xarray/compat/npcompat.py",
"*/xarray/compat/pdcompat.py",
"*/xarray/namedarray/pycompat.py",
"*/xarray/core/types.py",
Expand Down
5 changes: 2 additions & 3 deletions xarray/backends/writers.py
Original file line number Diff line number Diff line change
Expand Up @@ -747,9 +747,8 @@ def to_zarr(
# validate Dataset keys, DataArray names
_validate_dataset_names(dataset)

# Load empty arrays to avoid bug saving zero length dimensions (Issue #5741)
# TODO: delete when min dask>=2023.12.1
# https://github.com/dask/dask/pull/10506
# Load empty arrays, otherwise the zero-sized dask chunks become zero-sized
# zarr chunks, which zarr rejects (Issue #5741)
for v in dataset.variables.values():
if v.size == 0:
v.load()
Expand Down
2 changes: 1 addition & 1 deletion xarray/backends/zarr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1931,7 +1931,7 @@ def _get_open_params(
# TODO: replace AssertionError after https://github.com/zarr-developers/zarr-python/issues/2821 is resolved
missing_exc = AssertionError

# zarr 3.0.8 and earlier did not support this property - it was effectively assumed true
# store can also be a path or a mapping, which don't have this property
if not getattr(store, "supports_consolidated_metadata", True):
consolidated = consolidate_on_close = False

Expand Down
16 changes: 3 additions & 13 deletions xarray/coding/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,11 @@
unpack_for_encoding,
)
from xarray.core import indexing
from xarray.core.utils import emit_user_level_warning, module_available
from xarray.core.utils import emit_user_level_warning
from xarray.core.variable import Variable
from xarray.namedarray.parallelcompat import get_chunked_array_type
from xarray.namedarray.pycompat import is_chunked_array

HAS_NUMPY_2_0 = module_available("numpy", minversion="2.0.0.dev0")


def create_vlen_dtype(element_type):
if element_type not in (str, bytes):
Expand Down Expand Up @@ -199,12 +197,8 @@ def bytes_to_char(arr):

def _numpy_bytes_to_char(arr):
"""Like netCDF4.stringtochar, but faster and more flexible."""
# adapt handling of copy-kwarg to numpy 2.0
# see https://github.com/numpy/numpy/issues/25916
# and https://github.com/numpy/numpy/pull/25922
copy = None if HAS_NUMPY_2_0 else False
# ensure the array is contiguous
arr = np.array(arr, copy=copy, order="C", dtype=np.bytes_)
arr = np.array(arr, copy=None, order="C", dtype=np.bytes_)
return arr.reshape(arr.shape + (1,)).view("S1")


Expand Down Expand Up @@ -246,12 +240,8 @@ def char_to_bytes(arr):

def _numpy_char_to_bytes(arr):
"""Like netCDF4.chartostring, but faster and more flexible."""
# adapt handling of copy-kwarg to numpy 2.0
# see https://github.com/numpy/numpy/issues/25916
# and https://github.com/numpy/numpy/pull/25922
copy = None if HAS_NUMPY_2_0 else False
# based on: https://stackoverflow.com/a/10984878/809705
arr = np.array(arr, copy=copy, order="C")
arr = np.array(arr, copy=None, order="C")
dtype = "S" + str(arr.shape[-1])
return arr.view(dtype).reshape(arr.shape[:-1])

Expand Down
Loading
Loading