diff --git a/xarray/backends/cfgrib_.py b/xarray/backends/cfgrib_.py index 9e5546f052a..e7aeaaba83a 100644 --- a/xarray/backends/cfgrib_.py +++ b/xarray/backends/cfgrib_.py @@ -94,6 +94,8 @@ def get_encoding(self): class CfgribfBackendEntrypoint(BackendEntrypoint): + available = has_cfgrib + def guess_can_open(self, filename_or_obj): try: _, ext = os.path.splitext(filename_or_obj) @@ -147,5 +149,4 @@ def open_dataset( return ds -if has_cfgrib: - BACKEND_ENTRYPOINTS["cfgrib"] = CfgribfBackendEntrypoint +BACKEND_ENTRYPOINTS["cfgrib"] = CfgribfBackendEntrypoint diff --git a/xarray/backends/h5netcdf_.py b/xarray/backends/h5netcdf_.py index a6e04fe7567..3a49928ec65 100644 --- a/xarray/backends/h5netcdf_.py +++ b/xarray/backends/h5netcdf_.py @@ -337,6 +337,8 @@ def close(self, **kwargs): class H5netcdfBackendEntrypoint(BackendEntrypoint): + available = has_h5netcdf + def guess_can_open(self, filename_or_obj): magic_number = try_read_magic_number_from_file_or_path(filename_or_obj) if magic_number is not None: @@ -394,5 +396,4 @@ def open_dataset( return ds -if has_h5netcdf: - BACKEND_ENTRYPOINTS["h5netcdf"] = H5netcdfBackendEntrypoint +BACKEND_ENTRYPOINTS["h5netcdf"] = H5netcdfBackendEntrypoint diff --git a/xarray/backends/netCDF4_.py b/xarray/backends/netCDF4_.py index 95e8943dacb..769c96c99ce 100644 --- a/xarray/backends/netCDF4_.py +++ b/xarray/backends/netCDF4_.py @@ -512,6 +512,8 @@ def close(self, **kwargs): class NetCDF4BackendEntrypoint(BackendEntrypoint): + available = has_netcdf4 + def guess_can_open(self, filename_or_obj): if isinstance(filename_or_obj, str) and is_remote_uri(filename_or_obj): return True @@ -573,5 +575,4 @@ def open_dataset( return ds -if has_netcdf4: - BACKEND_ENTRYPOINTS["netcdf4"] = NetCDF4BackendEntrypoint +BACKEND_ENTRYPOINTS["netcdf4"] = NetCDF4BackendEntrypoint diff --git a/xarray/backends/plugins.py b/xarray/backends/plugins.py index 633459239c2..08c1bec8325 100644 --- a/xarray/backends/plugins.py +++ b/xarray/backends/plugins.py @@ -81,7 +81,10 @@ def sort_backends(backend_entrypoints): def build_engines(pkg_entrypoints): - backend_entrypoints = BACKEND_ENTRYPOINTS.copy() + backend_entrypoints = {} + for backend_name, backend in BACKEND_ENTRYPOINTS.items(): + if backend.available: + backend_entrypoints[backend_name] = backend pkg_entrypoints = remove_duplicates(pkg_entrypoints) external_backend_entrypoints = backends_dict_from_pkg(pkg_entrypoints) backend_entrypoints.update(external_backend_entrypoints) @@ -101,30 +104,49 @@ def guess_engine(store_spec): for engine, backend in engines.items(): try: - if backend.guess_can_open and backend.guess_can_open(store_spec): + if backend.guess_can_open(store_spec): return engine except Exception: warnings.warn(f"{engine!r} fails while guessing", RuntimeWarning) - installed = [k for k in engines if k != "store"] - if installed: - raise ValueError( - "did not find a match in any of xarray's currently installed IO " - f"backends {installed}. Consider explicitly selecting one of the " - "installed backends via the ``engine`` parameter to " - "xarray.open_dataset(), or installing additional IO dependencies:\n" - "http://xarray.pydata.org/en/stable/getting-started-guide/installing.html\n" - "http://xarray.pydata.org/en/stable/user-guide/io.html" - ) + compatible_engines = [] + for engine, backend_cls in BACKEND_ENTRYPOINTS.items(): + try: + backend = backend_cls() + if backend.guess_can_open(store_spec): + compatible_engines.append(engine) + except Exception: + warnings.warn(f"{engine!r} fails while guessing", RuntimeWarning) + + installed_engines = [k for k in engines if k != "store"] + if not compatible_engines: + if installed_engines: + error_msg = ( + "did not find a match in any of xarray's currently installed IO " + f"backends {installed_engines}. Consider explicitly selecting one of the " + "installed engines via the ``engine`` parameter, or installing " + "additional IO dependencies, see:\n" + "http://xarray.pydata.org/en/stable/getting-started-guide/installing.html\n" + "http://xarray.pydata.org/en/stable/user-guide/io.html" + ) + else: + error_msg = ( + "xarray is unable to open this file because it has no currently " + "installed IO backends. Xarray's read/write support requires " + "installing optional IO dependencies, see:\n" + "http://xarray.pydata.org/en/stable/getting-started-guide/installing.html\n" + "http://xarray.pydata.org/en/stable/user-guide/io" + ) else: - raise ValueError( - "xarray is unable to open this file because it has no currently " - "installed IO backends. Xarray's read/write support requires " - "installing optional dependencies:\n" - "http://xarray.pydata.org/en/stable/getting-started-guide/installing.html\n" - "http://xarray.pydata.org/en/stable/user-guide/io.html" + error_msg = ( + "found the following matches with the input file in xarray's IO " + f"backends: {compatible_engines}. But their dependencies may not be installed, see:\n" + "http://xarray.pydata.org/en/stable/user-guide/io.html \n" + "http://xarray.pydata.org/en/stable/getting-started-guide/installing.html" ) + raise ValueError(error_msg) + def get_backend(engine): """Select open_dataset method based on current engine.""" diff --git a/xarray/backends/pseudonetcdf_.py b/xarray/backends/pseudonetcdf_.py index c97c7141bd1..da178926dbe 100644 --- a/xarray/backends/pseudonetcdf_.py +++ b/xarray/backends/pseudonetcdf_.py @@ -102,6 +102,7 @@ def close(self): class PseudoNetCDFBackendEntrypoint(BackendEntrypoint): + available = has_pseudonetcdf # *args and **kwargs are not allowed in open_backend_dataset_ kwargs, # unless the open_dataset_parameters are explicity defined like this: @@ -153,5 +154,4 @@ def open_dataset( return ds -if has_pseudonetcdf: - BACKEND_ENTRYPOINTS["pseudonetcdf"] = PseudoNetCDFBackendEntrypoint +BACKEND_ENTRYPOINTS["pseudonetcdf"] = PseudoNetCDFBackendEntrypoint diff --git a/xarray/backends/pydap_.py b/xarray/backends/pydap_.py index 25d2df9d76a..bc479f9a71d 100644 --- a/xarray/backends/pydap_.py +++ b/xarray/backends/pydap_.py @@ -110,6 +110,8 @@ def get_dimensions(self): class PydapBackendEntrypoint(BackendEntrypoint): + available = has_pydap + def guess_can_open(self, filename_or_obj): return isinstance(filename_or_obj, str) and is_remote_uri(filename_or_obj) @@ -154,5 +156,4 @@ def open_dataset( return ds -if has_pydap: - BACKEND_ENTRYPOINTS["pydap"] = PydapBackendEntrypoint +BACKEND_ENTRYPOINTS["pydap"] = PydapBackendEntrypoint diff --git a/xarray/backends/pynio_.py b/xarray/backends/pynio_.py index bb57e0bea81..4e912f3e1ef 100644 --- a/xarray/backends/pynio_.py +++ b/xarray/backends/pynio_.py @@ -99,6 +99,8 @@ def close(self): class PynioBackendEntrypoint(BackendEntrypoint): + available = has_pynio + def open_dataset( self, filename_or_obj, @@ -112,13 +114,13 @@ def open_dataset( mode="r", lock=None, ): + filename_or_obj = _normalize_path(filename_or_obj) store = NioDataStore( filename_or_obj, mode=mode, lock=lock, ) - filename_or_obj = _normalize_path(filename_or_obj) store_entrypoint = StoreBackendEntrypoint() with close_on_error(store): ds = store_entrypoint.open_dataset( @@ -134,5 +136,4 @@ def open_dataset( return ds -if has_pynio: - BACKEND_ENTRYPOINTS["pynio"] = PynioBackendEntrypoint +BACKEND_ENTRYPOINTS["pynio"] = PynioBackendEntrypoint diff --git a/xarray/backends/scipy_.py b/xarray/backends/scipy_.py index 7394770cbe8..4c1ce1ef09d 100644 --- a/xarray/backends/scipy_.py +++ b/xarray/backends/scipy_.py @@ -238,6 +238,8 @@ def close(self): class ScipyBackendEntrypoint(BackendEntrypoint): + available = has_scipy + def guess_can_open(self, filename_or_obj): magic_number = try_read_magic_number_from_file_or_path(filename_or_obj) @@ -290,5 +292,4 @@ def open_dataset( return ds -if has_scipy: - BACKEND_ENTRYPOINTS["scipy"] = ScipyBackendEntrypoint +BACKEND_ENTRYPOINTS["scipy"] = ScipyBackendEntrypoint diff --git a/xarray/backends/store.py b/xarray/backends/store.py index 860a0254b64..b774d2bce95 100644 --- a/xarray/backends/store.py +++ b/xarray/backends/store.py @@ -4,6 +4,8 @@ class StoreBackendEntrypoint(BackendEntrypoint): + available = True + def guess_can_open(self, filename_or_obj): return isinstance(filename_or_obj, AbstractDataStore) diff --git a/xarray/backends/zarr.py b/xarray/backends/zarr.py index 72c4e99265d..5b228f77d24 100644 --- a/xarray/backends/zarr.py +++ b/xarray/backends/zarr.py @@ -703,6 +703,15 @@ def open_zarr( class ZarrBackendEntrypoint(BackendEntrypoint): + available = has_zarr + + def guess_can_open(self, filename_or_obj): + try: + _, ext = os.path.splitext(filename_or_obj) + except TypeError: + return False + return ext in {".zarr"} + def open_dataset( self, filename_or_obj, @@ -757,5 +766,4 @@ def open_dataset( return ds -if has_zarr: - BACKEND_ENTRYPOINTS["zarr"] = ZarrBackendEntrypoint +BACKEND_ENTRYPOINTS["zarr"] = ZarrBackendEntrypoint diff --git a/xarray/tests/test_plugins.py b/xarray/tests/test_plugins.py index b35971e185b..b7a5f9405d1 100644 --- a/xarray/tests/test_plugins.py +++ b/xarray/tests/test_plugins.py @@ -164,16 +164,20 @@ def test_build_engines_sorted(): mock.MagicMock(return_value={"dummy": DummyBackendEntrypointArgs()}), ) def test_no_matching_engine_found(): - with pytest.raises( - ValueError, match="match in any of xarray's currently installed IO" - ): + with pytest.raises(ValueError, match=r"did not find a match in any"): plugins.guess_engine("not-valid") + with pytest.raises(ValueError, match=r"found the following matches with the input"): + plugins.guess_engine("foo.nc") + @mock.patch( "xarray.backends.plugins.list_engines", mock.MagicMock(return_value={}), ) -def test_no_engines_installed(): - with pytest.raises(ValueError, match="no currently installed IO backends."): +def test_engines_not_installed(): + with pytest.raises(ValueError, match=r"xarray is unable to open"): plugins.guess_engine("not-valid") + + with pytest.raises(ValueError, match=r"found the following matches with the input"): + plugins.guess_engine("foo.nc")