diff --git a/doc/whats-new.rst b/doc/whats-new.rst index bace038bb17..f5469fa5227 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -32,7 +32,8 @@ Bug fixes - Fix transpose of boolean arrays read from disk. (:issue:`10536`) By `Deepak Cherian `_. - +- Fix detection of the ``h5netcdf`` backend. Xarray now selects ``h5netcdf`` if the default ``netCDF4`` engine is not available (:issue:`10401`, :pull:`10557`). + By `Scott Staniewicz `_. Documentation ~~~~~~~~~~~~~ diff --git a/xarray/backends/api.py b/xarray/backends/api.py index cfd3ff7fc0f..9aa37115a37 100644 --- a/xarray/backends/api.py +++ b/xarray/backends/api.py @@ -1,5 +1,6 @@ from __future__ import annotations +import importlib.util import os from collections.abc import ( Callable, @@ -122,23 +123,21 @@ def _get_default_engine_gz() -> Literal["scipy"]: return engine -def _get_default_engine_netcdf() -> Literal["netcdf4", "scipy"]: - engine: Literal["netcdf4", "scipy"] - try: - import netCDF4 # noqa: F401 +def _get_default_engine_netcdf() -> Literal["netcdf4", "h5netcdf", "scipy"]: + candidates: list[tuple[str, str]] = [ + ("netcdf4", "netCDF4"), + ("h5netcdf", "h5netcdf"), + ("scipy", "scipy.io.netcdf"), + ] - engine = "netcdf4" - except ImportError: # pragma: no cover - try: - import scipy.io.netcdf # noqa: F401 + for engine, module_name in candidates: + if importlib.util.find_spec(module_name) is not None: + return cast(Literal["netcdf4", "h5netcdf", "scipy"], engine) - engine = "scipy" - except ImportError as err: - raise ValueError( - "cannot read or write netCDF files without " - "netCDF4-python or scipy installed" - ) from err - return engine + raise ValueError( + "cannot read or write NetCDF files because none of " + "'netCDF4-python', 'h5netcdf', or 'scipy' are installed" + ) def _get_default_engine(path: str, allow_remote: bool = False) -> T_NetcdfEngine: diff --git a/xarray/tests/test_backends_api.py b/xarray/tests/test_backends_api.py index 778e800ec67..ed487b07450 100644 --- a/xarray/tests/test_backends_api.py +++ b/xarray/tests/test_backends_api.py @@ -1,16 +1,18 @@ from __future__ import annotations +import sys from numbers import Number import numpy as np import pytest import xarray as xr -from xarray.backends.api import _get_default_engine +from xarray.backends.api import _get_default_engine, _get_default_engine_netcdf from xarray.tests import ( assert_identical, assert_no_warnings, requires_dask, + requires_h5netcdf, requires_netCDF4, requires_scipy, ) @@ -29,6 +31,17 @@ def test__get_default_engine() -> None: assert engine_default == "netcdf4" +@requires_h5netcdf +def test_default_engine_h5netcdf(monkeypatch): + """Test the default netcdf engine when h5netcdf is the only importable module.""" + + monkeypatch.delitem(sys.modules, "netCDF4", raising=False) + monkeypatch.delitem(sys.modules, "scipy", raising=False) + monkeypatch.setattr(sys, "meta_path", []) + + assert _get_default_engine_netcdf() == "h5netcdf" + + def test_custom_engine() -> None: expected = xr.Dataset( dict(a=2 * np.arange(5)), coords=dict(x=("x", np.arange(5), dict(units="s")))