diff --git a/.claude/skills/upgrade-min-versions/SKILL.md b/.claude/skills/upgrade-min-versions/SKILL.md index 62746aa7ad0..2ee8f809747 100644 --- a/.claude/skills/upgrade-min-versions/SKILL.md +++ b/.claude/skills/upgrade-min-versions/SKILL.md @@ -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 @@ -93,12 +97,56 @@ 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_` + 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_:` 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): @@ -106,34 +154,63 @@ Add a table to `doc/whats-new.rst` under "Breaking Changes" documenting the vers 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:``): + + =========== ====== ====== + 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. diff --git a/ci/policy.yaml b/ci/policy.yaml index b9b1a42da35..2608545c228 100644 --- a/ci/policy.yaml +++ b/ci/policy.yaml @@ -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 diff --git a/doc/whats-new.rst b/doc/whats-new.rst index b73bbd0efad..44fdbce7e55 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -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 ` (:pull:`11649`). + By `Michael Niklas `_. +- 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 `_. - :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 diff --git a/pixi.toml b/pixi.toml index c1a3109a9bc..e9f288c6590 100644 --- a/pixi.toml +++ b/pixi.toml @@ -20,7 +20,7 @@ python = "*" numpy = "*" pandas = "*" -packaging = "24.2.*" +packaging = "25.0.*" git = "*" # needed for dynamic versioning [dependencies] @@ -101,20 +101,20 @@ 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 @@ -122,25 +122,25 @@ 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 = "*" diff --git a/pyproject.toml b/pyproject.toml index 36d4dc074b7..460bfed0661 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 @@ -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 @@ -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", @@ -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", diff --git a/xarray/backends/writers.py b/xarray/backends/writers.py index 71ad98b9126..205a2f83e80 100644 --- a/xarray/backends/writers.py +++ b/xarray/backends/writers.py @@ -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() diff --git a/xarray/backends/zarr.py b/xarray/backends/zarr.py index 415eff9c52d..c10b28a5ee8 100644 --- a/xarray/backends/zarr.py +++ b/xarray/backends/zarr.py @@ -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 diff --git a/xarray/coding/strings.py b/xarray/coding/strings.py index 3848364fb1a..1667810e8cd 100644 --- a/xarray/coding/strings.py +++ b/xarray/coding/strings.py @@ -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): @@ -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") @@ -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]) diff --git a/xarray/compat/dask_array_compat.py b/xarray/compat/dask_array_compat.py index 47d5da2b620..fad58c938eb 100644 --- a/xarray/compat/dask_array_compat.py +++ b/xarray/compat/dask_array_compat.py @@ -2,7 +2,6 @@ from xarray.namedarray.parallelcompat import get_chunked_array_type from xarray.namedarray.pycompat import is_chunked_array -from xarray.namedarray.utils import module_available def reshape_blockwise( @@ -19,18 +18,15 @@ def reshape_blockwise( if array_api is not None and hasattr(array_api, "reshape_blockwise"): return array_api.reshape_blockwise(x, shape=shape, chunks=chunks) - elif module_available("dask", "2024.08.2"): + else: from dask.array import reshape_blockwise as dask_reshape_blockwise return dask_reshape_blockwise(x, shape=shape, chunks=chunks) - else: - return x.reshape(shape) def sliding_window_view( x, window_shape, axis=None, *, automatic_rechunk=True, **kwargs ): - # Backcompat for handling `automatic_rechunk`, delete when dask>=2024.11.0 # Note that subok, writeable are unsupported by dask, so we ignore those in kwargs try: array_api = get_chunked_array_type(x).array_api @@ -51,10 +47,6 @@ def sliding_window_view( from dask.array.lib.stride_tricks import sliding_window_view - if module_available("dask", "2024.11.0"): - return sliding_window_view( - x, window_shape=window_shape, axis=axis, automatic_rechunk=automatic_rechunk - ) - else: - # automatic_rechunk is not supported - return sliding_window_view(x, window_shape=window_shape, axis=axis) + return sliding_window_view( + x, window_shape=window_shape, axis=axis, automatic_rechunk=automatic_rechunk + ) diff --git a/xarray/compat/npcompat.py b/xarray/compat/npcompat.py deleted file mode 100644 index cdd59093f65..00000000000 --- a/xarray/compat/npcompat.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (c) 2005-2011, NumPy Developers. -# All rights reserved. - -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: - -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. - -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. - -# * Neither the name of the NumPy Developers nor the names of any -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. - -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from __future__ import annotations - -from typing import Any - -try: - # requires numpy>=2.0 - from numpy import isdtype # type: ignore[attr-defined,unused-ignore] - - HAS_STRING_DTYPE = True -except ImportError: - import numpy as np - from numpy.typing import DTypeLike - - kind_mapping = { - "bool": np.bool_, - "signed integer": np.signedinteger, - "unsigned integer": np.unsignedinteger, - "integral": np.integer, - "real floating": np.floating, - "complex floating": np.complexfloating, - "numeric": np.number, - } - - def isdtype( # type: ignore[misc] - dtype: np.dtype[Any] | type[Any], kind: DTypeLike | tuple[DTypeLike, ...] - ) -> bool: - kinds = kind if isinstance(kind, tuple) else (kind,) - str_kinds = {k for k in kinds if isinstance(k, str)} - type_kinds = {k.type for k in kinds if isinstance(k, np.dtype)} - - if unknown_kind_types := set(kinds) - str_kinds - type_kinds: - raise TypeError( - f"kind must be str, np.dtype or a tuple of these, got {unknown_kind_types}" - ) - if unknown_kinds := {k for k in str_kinds if k not in kind_mapping}: - raise ValueError( - f"unknown kind: {unknown_kinds}, must be an np.dtype or one of {list(kind_mapping)}" - ) - - # verified the dtypes already, no need to check again - translated_kinds = {kind_mapping[k] for k in str_kinds} | type_kinds - if isinstance(dtype, np.generic): - return isinstance(dtype, translated_kinds) - else: - return any(np.issubdtype(dtype, k) for k in translated_kinds) - - HAS_STRING_DTYPE = False diff --git a/xarray/computation/fit.py b/xarray/computation/fit.py index cc7289ac647..e65c2463644 100644 --- a/xarray/computation/fit.py +++ b/xarray/computation/fit.py @@ -14,12 +14,7 @@ ) import numpy as np - -# remove once numpy 2.0 is the oldest supported version -try: - from numpy.exceptions import RankWarning -except ImportError: - from numpy import RankWarning # type: ignore[no-redef,attr-defined,unused-ignore] +from numpy.exceptions import RankWarning from xarray.computation.apply_ufunc import apply_ufunc from xarray.computation.computation import _ensure_numeric, where diff --git a/xarray/core/_aggregations.py b/xarray/core/_aggregations.py index 6ad8858423c..05a0c751429 100644 --- a/xarray/core/_aggregations.py +++ b/xarray/core/_aggregations.py @@ -5012,7 +5012,6 @@ def median( if ( flox_available and OPTIONS["use_flox"] - and module_available("flox", minversion="0.9.2") and contains_only_chunked_or_numpy(self._obj) ): kwargs.setdefault("method", "blockwise") @@ -6575,7 +6574,6 @@ def median( if ( flox_available and OPTIONS["use_flox"] - and module_available("flox", minversion="0.9.2") and contains_only_chunked_or_numpy(self._obj) ): kwargs.setdefault("method", "blockwise") @@ -8039,7 +8037,6 @@ def median( if ( flox_available and OPTIONS["use_flox"] - and module_available("flox", minversion="0.9.2") and contains_only_chunked_or_numpy(self._obj) ): kwargs.setdefault("method", "blockwise") @@ -9484,7 +9481,6 @@ def median( if ( flox_available and OPTIONS["use_flox"] - and module_available("flox", minversion="0.9.2") and contains_only_chunked_or_numpy(self._obj) ): kwargs.setdefault("method", "blockwise") diff --git a/xarray/core/common.py b/xarray/core/common.py index 3dda163442a..0f05f6b9869 100644 --- a/xarray/core/common.py +++ b/xarray/core/common.py @@ -181,17 +181,7 @@ def __array__( self: Any, dtype: DTypeLike | None = None, /, *, copy: bool | None = None ) -> np.ndarray: if not copy: - if np.lib.NumpyVersion(np.__version__) >= "2.0.0": - copy = None - elif np.lib.NumpyVersion(np.__version__) <= "1.28.0": - copy = False - else: - # 2.0.0 dev versions, handle cases where copy may or may not exist - try: - np.array([1]).__array__(copy=None) - copy = None - except TypeError: - copy = False + copy = None return np.array(self.values, dtype=dtype, copy=copy) def __repr__(self) -> str: diff --git a/xarray/core/dtypes.py b/xarray/core/dtypes.py index 51d0222241b..545aa99f765 100644 --- a/xarray/core/dtypes.py +++ b/xarray/core/dtypes.py @@ -7,8 +7,7 @@ import numpy as np from pandas.api.extensions import ExtensionDtype -from xarray.compat import array_api_compat, npcompat -from xarray.compat.npcompat import HAS_STRING_DTYPE +from xarray.compat import array_api_compat from xarray.core import utils from xarray.core.types import PDDatetimeUnitOptions @@ -77,7 +76,7 @@ def maybe_promote[T_dtype: (np.dtype, ExtensionDtype)]( raise TypeError( f"dtype {dtype} must be one of an extension array dtype or numpy dtype" ) - elif HAS_STRING_DTYPE and np.issubdtype(dtype, np.dtypes.StringDType()): + elif np.issubdtype(dtype, np.dtypes.StringDType()): # for now, we always promote string dtypes to object for consistency with existing behavior # TODO: refactor this once we have a better way to handle numpy vlen-string dtypes dtype_ = object @@ -231,7 +230,7 @@ def isdtype(dtype, kind: str | tuple[str, ...], xp=None) -> bool: raise TypeError(f"kind must be a string or a tuple of strings: {kind!r}") if isinstance(dtype, np.dtype): - return npcompat.isdtype(dtype, kind) + return np.isdtype(dtype, kind) elif utils.is_allowed_extension_array_dtype(dtype): # we never want to match pandas extension array dtypes return False diff --git a/xarray/core/duck_array_ops.py b/xarray/core/duck_array_ops.py index 90dfc5b78e0..7594ff2a560 100644 --- a/xarray/core/duck_array_ops.py +++ b/xarray/core/duck_array_ops.py @@ -23,10 +23,10 @@ take, unravel_index, # noqa: F401 ) +from numpy.lib.array_utils import normalize_axis_index from xarray.compat import dask_array_compat, dask_array_ops from xarray.compat.array_api_compat import get_array_namespace -from xarray.compat.npcompat import HAS_STRING_DTYPE from xarray.core import dtypes, nputils from xarray.core.extension_array import ( PandasExtensionArray, @@ -42,17 +42,6 @@ from xarray.namedarray.parallelcompat import get_chunked_array_type from xarray.namedarray.pycompat import array_type, is_chunked_array -# remove once numpy 2.0 is the oldest supported version -if module_available("numpy", minversion="2.0.0.dev0"): - from numpy.lib.array_utils import ( # type: ignore[import-not-found,unused-ignore] - normalize_axis_index, - ) -else: - from numpy.core.multiarray import ( # type: ignore[attr-defined,no-redef,unused-ignore] - normalize_axis_index, - ) - - dask_available = module_available("dask") @@ -189,7 +178,7 @@ def isnull(data): # note: must check timedelta64 before integers, because currently # timedelta64 inherits from np.integer return isnat(data) - elif HAS_STRING_DTYPE and isinstance(scalar_type, np.dtypes.StringDType): + elif isinstance(scalar_type, np.dtypes.StringDType): # na is settable, but it defaults to an empty string na_object = getattr(scalar_type, "na_object", "") if isna(na_object): diff --git a/xarray/core/extension_array.py b/xarray/core/extension_array.py index 87f2d673bd0..02c96a335e7 100644 --- a/xarray/core/extension_array.py +++ b/xarray/core/extension_array.py @@ -7,7 +7,6 @@ import numpy as np import pandas as pd -from packaging.version import Version from pandas.api.extensions import ExtensionArray, ExtensionDtype from pandas.api.types import is_scalar as pd_is_scalar @@ -312,10 +311,7 @@ def ndim(self) -> int: def __array__( self, dtype: np.typing.DTypeLike | None = None, /, *, copy: bool | None = None ) -> np.ndarray: - if Version(np.__version__) >= Version("2.0.0"): - return np.asarray(self.array, dtype=dtype, copy=copy) - else: - return np.asarray(self.array, dtype=dtype) + return np.asarray(self.array, dtype=dtype, copy=copy) def __getattr__(self, attr: str) -> Any: # with __deepcopy__ or __copy__, the object is first constructed and then the sub-objects are attached (see https://docs.python.org/3/library/copy.html) diff --git a/xarray/core/groupby.py b/xarray/core/groupby.py index 535e05cfedc..f7e4b8f1ebe 100644 --- a/xarray/core/groupby.py +++ b/xarray/core/groupby.py @@ -10,7 +10,6 @@ import numpy as np import pandas as pd -from packaging.version import Version from xarray.computation import ops from xarray.computation.apply_ufunc import apply_ufunc @@ -1123,7 +1122,6 @@ def _flox_reduce( **kwargs: Any, ) -> T_Xarray: """Adaptor function that translates our groupby API to that of flox.""" - import flox from flox.xarray import xarray_reduce if "keepdims" in kwargs: @@ -1157,12 +1155,6 @@ def _flox_reduce( if keep_attrs is None: keep_attrs = _get_keep_attrs(default=True) - if Version(flox.__version__) < Version("0.9") and not self._by_chunked: - # preserve current strategy (approximately) for dask groupby - # on older flox versions to prevent surprises. - # flox >=0.9 will choose this on its own. - kwargs.setdefault("method", "cohorts") - midx_grouping_vars: tuple[Hashable, ...] = () for grouper in self.groupers: name = grouper.name @@ -1488,7 +1480,7 @@ def quantile( method == "linear" and OPTIONS["use_flox"] and contains_only_chunked_or_numpy(self._obj) - and module_available("flox", minversion="0.9.4") + and module_available("flox") ): result = self._flox_reduce( func="quantile", q=q, dim=dim, keep_attrs=keep_attrs, skipna=skipna @@ -1546,7 +1538,7 @@ def _first_or_last( if keep_attrs is None: keep_attrs = _get_keep_attrs(default=True) if ( - module_available("flox", minversion="0.10.0") + module_available("flox") and OPTIONS["use_flox"] and contains_only_chunked_or_numpy(self._obj) ): diff --git a/xarray/core/indexing.py b/xarray/core/indexing.py index eaa9e32370b..f248924602a 100644 --- a/xarray/core/indexing.py +++ b/xarray/core/indexing.py @@ -14,9 +14,7 @@ import numpy as np import pandas as pd from numpy.typing import DTypeLike -from packaging.version import Version -from xarray.compat.npcompat import HAS_STRING_DTYPE from xarray.core import duck_array_ops from xarray.core.coordinate_transform import CoordinateTransform from xarray.core.nputils import NumpyVIndexAdapter @@ -605,10 +603,7 @@ def __array__( self, dtype: DTypeLike | None = None, /, *, copy: bool | None = None ) -> np.ndarray: # Leave casting to an array up to the underlying array type. - if Version(np.__version__) >= Version("2.0.0"): - return np.asarray(self.get_duck_array(), dtype=dtype, copy=copy) - else: - return np.asarray(self.get_duck_array(), dtype=dtype) + return np.asarray(self.get_duck_array(), dtype=dtype, copy=copy) def get_duck_array(self): return self.array @@ -690,11 +685,7 @@ def __init__(self, array, indexer_cls: type[ExplicitIndexer] = BasicIndexer): def __array__( self, dtype: DTypeLike | None = None, /, *, copy: bool | None = None ) -> np.ndarray: - if Version(np.__version__) >= Version("2.0.0"): - return np.asarray(to_numpy(self.get_duck_array()), dtype=dtype, copy=copy) - - else: - return np.asarray(to_numpy(self.get_duck_array()), dtype=dtype) + return np.asarray(to_numpy(self.get_duck_array()), dtype=dtype, copy=copy) def get_duck_array(self): return self.array.get_duck_array() @@ -1987,7 +1978,7 @@ def __init__( self._dtype = get_valid_numpy_dtype(array) elif is_allowed_extension_array_dtype(dtype): self._dtype = cast(pd.api.extensions.ExtensionDtype, dtype) - elif HAS_STRING_DTYPE and isinstance(dtype, pd.StringDtype): + elif isinstance(dtype, pd.StringDtype): self._dtype = np.dtypes.StringDType(na_object=dtype.na_value) else: self._dtype = np.dtype(cast(DTypeLike, dtype)) @@ -2028,10 +2019,7 @@ def __array__( # this might not be public API array = array.astype("object") - if Version(np.__version__) >= Version("2.0.0"): - return np.asarray(array.values, dtype=dtype, copy=copy) - else: - return np.asarray(array.values, dtype=dtype) + return np.asarray(array.values, dtype=dtype, copy=copy) @override def get_duck_array(self) -> np.ndarray | PandasExtensionArray: diff --git a/xarray/core/nputils.py b/xarray/core/nputils.py index 5c3742f4177..c9bf5ac80db 100644 --- a/xarray/core/nputils.py +++ b/xarray/core/nputils.py @@ -5,29 +5,12 @@ import numpy as np import pandas as pd -from packaging.version import Version +from numpy.exceptions import RankWarning +from numpy.lib.array_utils import normalize_axis_index from xarray.compat.array_api_compat import get_array_namespace -from xarray.core.utils import is_duck_array, module_available -from xarray.namedarray import pycompat - -# remove once numpy 2.0 is the oldest supported version -if module_available("numpy", minversion="2.0.0.dev0"): - from numpy.lib.array_utils import ( # type: ignore[import-not-found,unused-ignore] - normalize_axis_index, - ) -else: - from numpy.core.multiarray import ( # type: ignore[attr-defined,no-redef,unused-ignore] - normalize_axis_index, - ) - -# remove once numpy 2.0 is the oldest supported version -try: - from numpy.exceptions import RankWarning # type: ignore[attr-defined,unused-ignore] -except ImportError: - from numpy import RankWarning # type: ignore[attr-defined,no-redef,unused-ignore] - from xarray.core.options import OPTIONS +from xarray.core.utils import is_duck_array, module_available try: import bottleneck as bn @@ -189,24 +172,12 @@ def f(values, axis=None, **kwargs): module_available("numbagg") and OPTIONS["use_numbagg"] and isinstance(values, np.ndarray) - # numbagg<0.7.0 uses ddof=1 only, but numpy uses ddof=0 by default - and ( - pycompat.mod_version("numbagg") >= Version("0.7.0") - or ("var" not in name and "std" not in name) - or kwargs.get("ddof", 0) == 1 - ) # TODO: bool? and values.dtype.kind in "uif" # and values.dtype.isnative and (dtype is None or np.dtype(dtype) == values.dtype) - # numbagg.nanquantile only available after 0.8.0 and with linear method - and ( - name != "nanquantile" - or ( - pycompat.mod_version("numbagg") >= Version("0.8.0") - and kwargs.get("method", "linear") == "linear" - ) - ) + # numbagg.nanquantile only supports the linear method + and (name != "nanquantile" or kwargs.get("method", "linear") == "linear") ): import numbagg # type: ignore[import-not-found, unused-ignore] @@ -214,10 +185,6 @@ def f(values, axis=None, **kwargs): if nba_func is not None: # numbagg does not use dtype kwargs.pop("dtype", None) - # prior to 0.7.0, numbagg did not support ddof; we ensure it's limited - # to ddof=1 above. - if pycompat.mod_version("numbagg") < Version("0.7.0"): - kwargs.pop("ddof", None) if name == "nanquantile": kwargs["quantiles"] = kwargs.pop("q") kwargs.pop("method", None) diff --git a/xarray/namedarray/daskmanager.py b/xarray/namedarray/daskmanager.py index 6688bd3ba43..07bea48fd5c 100644 --- a/xarray/namedarray/daskmanager.py +++ b/xarray/namedarray/daskmanager.py @@ -272,10 +272,6 @@ def shuffle( ) -> DaskArray: import dask.array - if not module_available("dask", minversion="2024.08.1"): - raise ValueError( - "This method is very inefficient on dask<2024.08.1. Please upgrade." - ) if chunks is None: chunks = "auto" if chunks != "auto": diff --git a/xarray/plot/dataarray_plot.py b/xarray/plot/dataarray_plot.py index 554a89fa5e2..06edc365f32 100644 --- a/xarray/plot/dataarray_plot.py +++ b/xarray/plot/dataarray_plot.py @@ -1683,13 +1683,6 @@ def newplotfunc( if plotfunc.__name__ == "surface" and not kwargs.get("_is_facetgrid"): if ax is None: - # TODO: Importing Axes3D is no longer necessary in matplotlib >= 3.2. - # Remove when minimum requirement of matplotlib is 3.2: - from mpl_toolkits.mplot3d import Axes3D - - # delete so it does not end up in locals() - del Axes3D - # Need to create a "3d" Axes instance for surface plots subplot_kws["projection"] = "3d" diff --git a/xarray/structure/concat.py b/xarray/structure/concat.py index 9609dbcb5a9..a6524cff4f0 100644 --- a/xarray/structure/concat.py +++ b/xarray/structure/concat.py @@ -746,8 +746,7 @@ def get_indexes(name): yield PandasIndex(data, dim_name, coord_dtype=var.dtype) # create concatenation index, needed for later reindexing - # use np.cumulative_sum(concat_dim_lengths, include_initial=True) when we support numpy>=2 - file_start_indexes = np.append(0, np.cumsum(concat_dim_lengths)) + file_start_indexes = np.cumulative_sum(concat_dim_lengths, include_initial=True) concat_index_size = file_start_indexes[-1] variable_index_mask = np.ones(concat_index_size, dtype=bool) variable_reindexer = None diff --git a/xarray/tests/__init__.py b/xarray/tests/__init__.py index 5d7b692e592..cf0374d7684 100644 --- a/xarray/tests/__init__.py +++ b/xarray/tests/__init__.py @@ -102,7 +102,6 @@ def get_dask_chunkmanager(): has_matplotlib, requires_matplotlib = _importorskip("matplotlib") has_scipy, requires_scipy = _importorskip("scipy") -has_scipy_ge_1_13, requires_scipy_ge_1_13 = _importorskip("scipy", "1.13") with warnings.catch_warnings(): warnings.filterwarnings( "ignore", @@ -123,22 +122,6 @@ def get_dask_chunkmanager(): has_cftime, requires_cftime = _importorskip("cftime") has_dask, requires_dask = _importorskip("dask") has_dask_array, requires_dask_array = _importorskip("dask_array") -has_dask_ge_2024_08_1, requires_dask_ge_2024_08_1 = _importorskip( - "dask", minversion="2024.08.1" -) -has_dask_ge_2024_11_0, requires_dask_ge_2024_11_0 = _importorskip("dask", "2024.11.0") -has_dask_ge_2025_1_0, requires_dask_ge_2025_1_0 = _importorskip("dask", "2025.1.0") -if has_dask_ge_2025_1_0: - has_dask_expr = True - requires_dask_expr = pytest.mark.skipif(not has_dask_expr, reason="should not skip") -else: - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", - message="The current Dask DataFrame implementation is deprecated.", - category=FutureWarning, - ) - has_dask_expr, requires_dask_expr = _importorskip("dask_expr") if has_dask: dask_chunkmanager = get_dask_chunkmanager() @@ -158,11 +141,6 @@ def get_dask_chunkmanager(): "ignore", message="crc32c usage is deprecated", category=DeprecationWarning ) has_zarr, requires_zarr = _importorskip("zarr") -# Since min zarr is now >=3.0, these are aliases for has_zarr/requires_zarr. -# Kept to avoid a large diff across test files. -has_zarr_v3 = has_zarr -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") has_zarr_rectilinear_chunks, requires_zarr_rectilinear_chunks = _importorskip( "zarr", "3.2.0" @@ -174,12 +152,8 @@ def get_dask_chunkmanager(): # TODO: use version specifier # installing from git main is giving me a lower version than the # most recently released zarr - has_zarr_v3_dtypes = hasattr(zarr.core, "dtype") has_zarr_v3_async_oindex = hasattr(zarr.AsyncArray, "oindex") - requires_zarr_v3_dtypes = pytest.mark.skipif( - not has_zarr_v3_dtypes, reason="requires zarr>3.1.0" - ) requires_zarr_v3_async_oindex = pytest.mark.skipif( not has_zarr_v3_async_oindex, reason="requires zarr>3.1.1" ) @@ -222,8 +196,6 @@ def get_dask_chunkmanager(): requires_numbagg_or_bottleneck = pytest.mark.skipif( not has_numbagg_or_bottleneck, reason="requires numbagg or bottleneck" ) -has_numpy_2, requires_numpy_2 = _importorskip("numpy", "2.0.0") -has_flox_0_9_12, requires_flox_0_9_12 = _importorskip("flox", "0.9.12") has_array_api_strict, requires_array_api_strict = _importorskip("array_api_strict") has_jax, requires_jax = _importorskip("jax") @@ -259,14 +231,6 @@ def _importorskip_h5netcdf_ros3(has_h5netcdf: bool): has_h5netcdf_ros3, requires_h5netcdf_ros3 = _importorskip_h5netcdf_ros3(has_h5netcdf) -has_netCDF4_1_6_2_or_above, requires_netCDF4_1_6_2_or_above = _importorskip( - "netCDF4", "1.6.2" -) - -has_netCDF4_1_7_0_or_above, requires_netCDF4_1_7_0_or_above = _importorskip( - "netCDF4", "1.7.0" -) - # change some global options for tests set_options(warn_for_unclosed_files=True) diff --git a/xarray/tests/backends/base.py b/xarray/tests/backends/base.py index 1407414463b..604a5467625 100644 --- a/xarray/tests/backends/base.py +++ b/xarray/tests/backends/base.py @@ -26,7 +26,6 @@ from xarray.coders import CFTimedeltaCoder from xarray.coding.strings import check_vlen_dtype, create_vlen_dtype from xarray.coding.variables import SerializationWarning -from xarray.compat.npcompat import HAS_STRING_DTYPE from xarray.conventions import encode_dataset_coordinates from xarray.core import indexing from xarray.core.indexes import PandasIndex @@ -550,7 +549,6 @@ def test_roundtrip_string_data(self) -> None: with self.roundtrip(expected) as actual: assert_identical(expected, actual) - @pytest.mark.skipif(not HAS_STRING_DTYPE, reason="requires StringDType") def test_roundtrip_stringdtype_data(self) -> None: # GH11199 data = np.array(["ab", "cdef"], dtype=np.dtypes.StringDType()) @@ -558,7 +556,6 @@ def test_roundtrip_stringdtype_data(self) -> None: with self.roundtrip(expected) as actual: assert_identical(expected, actual) - @pytest.mark.skipif(not HAS_STRING_DTYPE, reason="requires StringDType") def test_roundtrip_stringdtype_nulls(self) -> None: # GH11199 — null values in StringDType are written as empty strings data = np.array(["ab", None], dtype=np.dtypes.StringDType(na_object=None)) @@ -567,7 +564,6 @@ def test_roundtrip_stringdtype_nulls(self) -> None: expected = Dataset({"x": ("t", np.array(["ab", ""]))}) assert_identical(expected, actual) - @pytest.mark.skipif(not HAS_STRING_DTYPE, reason="requires StringDType") def test_roundtrip_stringdtype_with_na_object(self) -> None: # GH11199 — StringDType(na_object="") should roundtrip correctly data = np.array(["ab", "cdef"], dtype=np.dtypes.StringDType(na_object="")) @@ -975,10 +971,8 @@ def test_roundtrip_empty_vlen_string_array(self) -> None: # eg. NETCDF3 based backends do not roundtrip metadata if actual["a"].dtype.metadata is not None: assert check_vlen_dtype(actual["a"].dtype) is str - elif HAS_STRING_DTYPE: - assert np.issubdtype(actual["a"].dtype, np.dtypes.StringDType()) else: - assert np.issubdtype(actual["a"].dtype, np.dtype("=U1")) + assert np.issubdtype(actual["a"].dtype, np.dtypes.StringDType()) @pytest.mark.parametrize("decoded_fn, encoded_fn", MASK_AND_SCALE_DATA) @pytest.mark.parametrize("dtype", [np.dtype("float64"), np.dtype("float32")]) diff --git a/xarray/tests/backends/test_api.py b/xarray/tests/backends/test_api.py index 84b6cc6173d..823b821ee40 100644 --- a/xarray/tests/backends/test_api.py +++ b/xarray/tests/backends/test_api.py @@ -44,7 +44,6 @@ assert_no_warnings, dask_array_type, has_netCDF4, - has_numpy_2, has_scipy, requires_aiobotocore, requires_cftime, @@ -1921,9 +1920,7 @@ def test_use_cftime_true(calendar, units_year) -> None: @requires_scipy_or_netCDF4 @pytest.mark.parametrize("calendar", _STANDARD_CALENDARS) -@pytest.mark.xfail( - has_numpy_2, reason="https://github.com/pandas-dev/pandas/issues/56996" -) +@pytest.mark.xfail(reason="https://github.com/pandas-dev/pandas/issues/56996") def test_use_cftime_false_standard_calendar_in_range(calendar) -> None: x = [0, 1] time = [0, 720] diff --git a/xarray/tests/backends/test_netCDF4.py b/xarray/tests/backends/test_netCDF4.py index a4e2f65a303..44ab0d29352 100644 --- a/xarray/tests/backends/test_netCDF4.py +++ b/xarray/tests/backends/test_netCDF4.py @@ -26,8 +26,6 @@ requires_h5netcdf, requires_h5netcdf_or_netCDF4, requires_netCDF4, - requires_netCDF4_1_6_2_or_above, - requires_netCDF4_1_7_0_or_above, ) from xarray.tests.backends.base import ( ON_WINDOWS, @@ -219,7 +217,6 @@ def test_setncattr_string(self) -> None: ), ], ) - @requires_netCDF4_1_6_2_or_above @pytest.mark.xfail(ON_WINDOWS, reason="new compression not yet implemented") def test_compression_encoding(self, compression: str | None) -> None: data = create_test_data(dim_sizes=(20, 80, 10)) @@ -277,7 +274,6 @@ def test_compression_encoding(self, compression: str | None) -> None: def test_refresh_from_disk(self) -> None: super().test_refresh_from_disk() - @requires_netCDF4_1_7_0_or_above def test_roundtrip_complex(self): expected = Dataset({"x": ("y", np.ones(5) + 1j * np.ones(5))}) skwargs = dict(auto_complex=True) diff --git a/xarray/tests/backends/test_zarr.py b/xarray/tests/backends/test_zarr.py index ef937b49076..c68a99d8f0d 100644 --- a/xarray/tests/backends/test_zarr.py +++ b/xarray/tests/backends/test_zarr.py @@ -33,7 +33,6 @@ from xarray.coders import CFDatetimeCoder, CFTimedeltaCoder from xarray.coding.cftime_offsets import date_range from xarray.coding.variables import SerializationWarning -from xarray.compat.npcompat import HAS_STRING_DTYPE from xarray.core.indexes import PandasIndex from xarray.tests import ( assert_equal, @@ -44,7 +43,6 @@ has_netCDF4, has_zarr, has_zarr_v3_async_oindex, - has_zarr_v3_dtypes, parametrize_zarr_format, requires_cftime, requires_dask, @@ -53,9 +51,7 @@ requires_netCDF4, requires_zarr, requires_zarr_rectilinear_chunks, - requires_zarr_v3, requires_zarr_v3_async_oindex, - requires_zarr_v3_dtypes, ) from xarray.tests.backends.base import ( DATA_DIR, @@ -132,9 +128,6 @@ def test_roundtrip_mask_and_scale(self, decoded_fn, encoded_fn, dtype) -> None: def test_encoding_unlimited_dims(self) -> None: super().test_encoding_unlimited_dims() - @skip_if_zarr_format_3( - "endian support requires zarr-python>=3.1", condition=not has_zarr_v3_dtypes - ) def test_roundtrip_endian(self) -> None: super().test_roundtrip_endian() @@ -778,9 +771,6 @@ def test_append_with_existing_encoding_raises(self) -> None: **self.version_kwargs, ) - @skip_if_zarr_format_3( - "This actually works fine with Zarr format 3", condition=not has_zarr_v3_dtypes - ) @pytest.mark.parametrize("dtype", ["U", "S"]) def test_append_string_length_mismatch_raises(self, dtype) -> None: ds, ds_to_append = create_append_string_length_mismatch_test_data(dtype) @@ -791,24 +781,6 @@ def test_append_string_length_mismatch_raises(self, dtype) -> None: store_target, append_dim="time", **self.version_kwargs ) - @pytest.mark.skipif( - has_zarr_v3_dtypes, - reason="This works on pre ZDtype Zarr-Python, but fails after.", - ) - # ...but it probably would work with Zarr format 2 if we used object dtype - @skip_if_zarr_format_2("This doesn't work with Zarr format 2") - @pytest.mark.parametrize("dtype", ["U", "S"]) - def test_append_string_length_mismatch_works(self, dtype) -> None: - - ds, ds_to_append = create_append_string_length_mismatch_test_data(dtype) - expected = xr.concat([ds, ds_to_append], dim="time") - - with self.create_zarr_target() as store_target: - ds.to_zarr(store_target, mode="w", **self.version_kwargs) - ds_to_append.to_zarr(store_target, append_dim="time", **self.version_kwargs) - actual = xr.open_dataset(store_target, engine="zarr") - xr.testing.assert_identical(expected, actual) - def test_check_encoding_is_consistent_after_append(self) -> None: ds, ds_to_append, _ = create_append_test_data() @@ -1542,8 +1514,7 @@ def test_region_write(self) -> None: self.check_requests(expected, patches) -@requires_zarr_v3_dtypes -@pytest.mark.skipif(not HAS_STRING_DTYPE, reason="requires StringDType") +@requires_zarr def test_roundtrip_stringdtype_zarr_v3() -> None: dtype = np.dtypes.StringDType() data = np.array(["a", "bb", "ccc"], dtype=dtype) @@ -1569,8 +1540,7 @@ def test_roundtrip_stringdtype_zarr_v3() -> None: assert_identical(expected, actual) -@requires_zarr_v3_dtypes -@pytest.mark.skipif(not HAS_STRING_DTYPE, reason="requires StringDType") +@requires_zarr def test_stringdtype_with_na_object_uses_the_compatibility_path() -> None: dtype = np.dtypes.StringDType(na_object=np.nan) data = np.array(["a", "bb", "ccc"], dtype=dtype) @@ -1616,7 +1586,6 @@ def test_chunk_key_encoding_v2(self) -> None: assert actual["var1"].encoding["chunks"] == (2, 2) @pytest.mark.asyncio - @requires_zarr_v3 async def test_async_load_multiple_variables(self) -> None: target_class = zarr.AsyncArray method_name = "getitem" @@ -1645,7 +1614,6 @@ async def test_async_load_multiple_variables(self) -> None: xrt.assert_identical(result_ds, ds.load()) @pytest.mark.asyncio - @requires_zarr_v3 @pytest.mark.parametrize("cls_name", ["Variable", "DataArray", "Dataset"]) async def test_concurrent_load_multiple_objects( self, @@ -1683,7 +1651,6 @@ async def test_concurrent_load_multiple_objects( xrt.assert_identical(result, xr_obj.load()) @pytest.mark.asyncio - @requires_zarr_v3 @pytest.mark.skip_if_param( cls_name="Variable", method="sel", reason="Variable doesn't have a .sel method" ) @@ -1881,11 +1848,10 @@ def with_read_only( ) -@requires_zarr_v3 +@requires_zarr class TestZarrNoConsolidatedMetadataSupport(ZarrBase): @contextlib.contextmanager def create_zarr_target(self): - # TODO the zarr version would need to be >3.08 for the supports_consolidated_metadata property to have any effect yield NoConsolidatedMetadataSupportStore( zarr.storage.MemoryStore({}, read_only=False) ) diff --git a/xarray/tests/test_accessor_dt.py b/xarray/tests/test_accessor_dt.py index 093236a5b26..17d9994847e 100644 --- a/xarray/tests/test_accessor_dt.py +++ b/xarray/tests/test_accessor_dt.py @@ -8,7 +8,6 @@ from xarray.tests import ( _CFTIME_CALENDARS, _all_cftime_date_types, - assert_allclose, assert_array_equal, assert_chunks_equal, assert_equal, @@ -117,8 +116,7 @@ def test_total_seconds(self) -> None: name="total_seconds", coords=[self.data.time], ) - # This works with assert_identical when pandas is >=1.5.0. - assert_allclose(expected, actual) + assert_identical(expected, actual) @pytest.mark.parametrize( "field, pandas_field", diff --git a/xarray/tests/test_dataarray.py b/xarray/tests/test_dataarray.py index 7b41dc35d6a..9d33c28a142 100644 --- a/xarray/tests/test_dataarray.py +++ b/xarray/tests/test_dataarray.py @@ -13,12 +13,8 @@ import numpy as np import pandas as pd import pytest - -# remove once numpy 2.0 is the oldest supported version -try: - from numpy.exceptions import RankWarning -except ImportError: - from numpy import RankWarning # type: ignore[no-redef,attr-defined,unused-ignore] +from numpy.exceptions import RankWarning +from pandas.errors import UndefinedVariableError import xarray as xr import xarray.core.missing @@ -51,13 +47,11 @@ assert_no_warnings, dask_array_api, dask_array_type, - has_dask_ge_2025_1_0, has_pyarrow, raise_if_dask_computes, requires_bottleneck, requires_cupy, requires_dask, - requires_dask_expr, requires_iris, requires_numexpr, requires_pint, @@ -69,13 +63,6 @@ ) from xarray.tests.indexes import XYIndex -try: - from pandas.errors import UndefinedVariableError -except ImportError: - # TODO: remove once we stop supporting pandas<1.4.3 - from pandas.core.computation.ops import UndefinedVariableError - - pytestmark = [ pytest.mark.filterwarnings("error:Mean of empty slice"), pytest.mark.filterwarnings("error:All-NaN (slice|axis) encountered"), @@ -3904,9 +3891,7 @@ def test_to_dataframe_coord_dtypes_2d(self, x_dtype, y_dtype, v_dtype) -> None: f"v data: expected {v_dtype}, got {df['v'].dtype}" ) - @requires_dask_expr @requires_dask - @pytest.mark.xfail(not has_dask_ge_2025_1_0, reason="dask-expr is broken") def test_to_dask_dataframe(self) -> None: arr_np = np.arange(3 * 4).reshape(3, 4) arr = DataArray(arr_np, [("B", [1, 2, 3]), ("A", list("cdef"))], name="foo") diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py index 578e4521c99..77f58564e4f 100644 --- a/xarray/tests/test_dataset.py +++ b/xarray/tests/test_dataset.py @@ -13,15 +13,9 @@ import numpy as np import pandas as pd import pytest +from numpy import trapezoid +from numpy.exceptions import RankWarning from pandas.core.indexes.datetimes import DatetimeIndex - -# remove once numpy 2.0 is the oldest supported version -try: - from numpy.exceptions import RankWarning -except ImportError: - from numpy import RankWarning # type: ignore[no-redef,attr-defined,unused-ignore] - - from pandas.errors import UndefinedVariableError import xarray as xr @@ -76,15 +70,6 @@ ) from xarray.tests.indexes import ScalarIndex, XYIndex -# from numpy version 2.0 trapz is deprecated and renamed to trapezoid -# remove once numpy 2.0 is the oldest supported version -try: - from numpy import trapezoid # type: ignore[attr-defined,unused-ignore] -except ImportError: - from numpy import ( # type: ignore[arg-type,no-redef,attr-defined,unused-ignore] - trapz as trapezoid, - ) - sparse_array_type = array_type("sparse") pytestmark = [ @@ -4842,8 +4827,7 @@ def test_setitem(self) -> None: data4[{"dim2": [2, 3]}] = data3["var1"][{"dim2": [3, 4]}].values data5 = data4.astype(str) data5["var4"] = data4["var1"] - # convert to `np.str_('a')` once `numpy<2.0` has been dropped - err_msg = "could not convert string to float: .*'a'.*" + err_msg = r"could not convert string to float: np\.str_\('a'\)" with pytest.raises(ValueError, match=err_msg): data5[{"dim2": 1}] = "a" diff --git a/xarray/tests/test_duck_array_ops.py b/xarray/tests/test_duck_array_ops.py index ab0dd426499..3d9cfc01a7d 100644 --- a/xarray/tests/test_duck_array_ops.py +++ b/xarray/tests/test_duck_array_ops.py @@ -14,7 +14,6 @@ from xarray import DataArray, Dataset, concat, date_range from xarray.coding.times import _NS_PER_TIME_DELTA -from xarray.compat.npcompat import HAS_STRING_DTYPE from xarray.core import dtypes, duck_array_ops from xarray.core.duck_array_ops import ( array_notnull_equiv, @@ -785,7 +784,6 @@ def test_dask_or_eager_func_does_not_fallback_to_legacy_dask_array(func): assert isinstance(result, dask_array_type) -@pytest.mark.skipif(not HAS_STRING_DTYPE, reason="requires StringDType to exist") @pytest.mark.parametrize( ["input", "na_object", "expected"], [ @@ -813,7 +811,6 @@ def test_isnull_with_different_StringDType_na_objects(input, na_object, expected np.testing.assert_equal(actual, expected) -@pytest.mark.skipif(not HAS_STRING_DTYPE, reason="requires StringDType to exist") def test_isnull_with_default_StringDType(): dtype = np.dtypes.StringDType() array = np.array(["a", np.nan, "c"], dtype=dtype) diff --git a/xarray/tests/test_groupby.py b/xarray/tests/test_groupby.py index 296e4debdc5..6485367ffe6 100644 --- a/xarray/tests/test_groupby.py +++ b/xarray/tests/test_groupby.py @@ -46,9 +46,7 @@ raise_if_dask_computes, requires_cftime, requires_dask, - requires_dask_ge_2024_08_1, requires_flox, - requires_flox_0_9_12, requires_scipy, ) @@ -3260,7 +3258,7 @@ def test_multiple_groupers_lazy(use_flox: bool) -> None: gb.count() -@requires_flox_0_9_12 +@requires_flox @pytest.mark.parametrize( "reduction", ["max", "min", "nanmax", "nanmin", "sum", "nansum", "prod", "nanprod"] ) @@ -3290,7 +3288,7 @@ def test_groupby_preserve_dtype(reduction): @requires_dask -@requires_flox_0_9_12 +@requires_flox @pytest.mark.parametrize("reduction", ["any", "all", "count"]) @pytest.mark.xfail_with_dask_array( reason="flox resample currently mixes legacy dask arrays" @@ -3502,7 +3500,7 @@ def test_groupby_multiple_bin_grouper_missing_groups() -> None: assert_identical(actual, expected) -@requires_dask_ge_2024_08_1 +@requires_dask def test_shuffle_simple() -> None: array_api = dask_array_api @@ -3519,7 +3517,7 @@ def test_shuffle_simple() -> None: da.chunk(x=2, eagerly_load_group=False).groupby("label").shuffle_to_chunks() -@requires_dask_ge_2024_08_1 +@requires_dask @pytest.mark.parametrize( "chunks, expected_chunks", [ diff --git a/xarray/tests/test_interp.py b/xarray/tests/test_interp.py index f779c6fe396..b68d9b474f6 100644 --- a/xarray/tests/test_interp.py +++ b/xarray/tests/test_interp.py @@ -25,7 +25,6 @@ requires_cftime, requires_dask, requires_scipy, - requires_scipy_ge_1_13, ) from xarray.tests.test_dataset import create_test_data @@ -178,11 +177,7 @@ def func(obj, new_x): ( (False, "linear"), (False, "akima"), - pytest.param( - False, - "makima", - marks=requires_scipy_ge_1_13, - ), + (False, "makima"), pytest.param( True, "linear", diff --git a/xarray/tests/test_rolling.py b/xarray/tests/test_rolling.py index 24f6e266c1e..7791c701b93 100644 --- a/xarray/tests/test_rolling.py +++ b/xarray/tests/test_rolling.py @@ -15,7 +15,6 @@ dask_array_api, requires_bottleneck, requires_dask, - requires_dask_ge_2024_11_0, requires_numbagg, ) @@ -475,7 +474,7 @@ def test_rolling_dask_dtype(self, dtype) -> None: assert chunked_result.dtype == unchunked_result.dtype @requires_bottleneck - @requires_dask_ge_2024_11_0 + @requires_dask @pytest.mark.parametrize( "name, center, dtype", [ @@ -717,7 +716,7 @@ def test_rolling_properties(self, ds) -> None: ): ds.rolling(foo=2) - @requires_dask_ge_2024_11_0 + @requires_dask def test_rolling_construct_automatic_rechunk(self): da = dask_array_api diff --git a/xarray/tests/test_ufuncs.py b/xarray/tests/test_ufuncs.py index a5a73480cb5..8bdedf4597e 100644 --- a/xarray/tests/test_ufuncs.py +++ b/xarray/tests/test_ufuncs.py @@ -14,7 +14,6 @@ assert_array_equal, mock, requires_dask, - requires_numpy_2, ) from xarray.tests import assert_identical as assert_identical_ @@ -218,13 +217,7 @@ def setUp(self): self.xt = xr.DataArray(np.datetime64("2021-01-01", "ns")) @pytest.mark.filterwarnings("ignore::RuntimeWarning") - @pytest.mark.parametrize( - "name", - [ - pytest.param(name, marks=() if hasattr(np, name) else requires_numpy_2) - for name in xu.__all__ - ], - ) + @pytest.mark.parametrize("name", xu.__all__) def test_ufuncs(self, name, request): xu_func = getattr(xu, name) np_func = getattr(np, name, None) diff --git a/xarray/tests/test_variable.py b/xarray/tests/test_variable.py index 723aa753bd4..64c8b1ce21e 100644 --- a/xarray/tests/test_variable.py +++ b/xarray/tests/test_variable.py @@ -46,7 +46,6 @@ assert_no_warnings, dask_array_type, has_dask_array_expr, - has_dask_ge_2024_11_0, has_pandas_3, raise_if_dask_computes, requires_bottleneck, @@ -1999,16 +1998,10 @@ def test_quantile_interpolation_deprecation(self, method) -> None: def test_quantile_chunked_dim_error(self): v = Variable(["x", "y"], self.d).chunk({"x": 2}) - if has_dask_ge_2024_11_0: - # Dask rechunks - np.testing.assert_allclose( - v.compute().quantile(0.5, dim="x"), v.quantile(0.5, dim="x") - ) - - else: - # this checks for ValueError in dask.array.apply_gufunc - with pytest.raises(ValueError, match=r"consists of multiple chunks"): - v.quantile(0.5, dim="x") + # Dask rechunks + np.testing.assert_allclose( + v.compute().quantile(0.5, dim="x"), v.quantile(0.5, dim="x") + ) @pytest.mark.parametrize("compute_backend", ["numbagg", None], indirect=True) @pytest.mark.parametrize("q", [-0.1, 1.1, [2], [0.25, 2]]) diff --git a/xarray/util/generate_aggregations.py b/xarray/util/generate_aggregations.py index 52c058515d8..eaa01347fd7 100644 --- a/xarray/util/generate_aggregations.py +++ b/xarray/util/generate_aggregations.py @@ -617,9 +617,7 @@ def generate_code(self, method, has_keep_attrs): numeric_only=True, ), Method("var", long_name="variance", extra_kwargs=(skipna, ddof), numeric_only=True), - Method( - "median", extra_kwargs=(skipna,), numeric_only=True, min_flox_version="0.9.2" - ), + Method("median", extra_kwargs=(skipna,), numeric_only=True), # Scans: Method( "cumsum",