Skip to content

Commit 160a85c

Browse files
kkollsgaclaude
andauthored
Raise FileNotFoundError for missing local files in open_dataset (#10896) (#11150)
When `open_dataset` is called with a non-existent local file path, `guess_engine` now raises `FileNotFoundError` instead of a confusing "no backend found" `ValueError`. Remote URIs are not affected. Co-authored-by: Claude <noreply@anthropic.com>
1 parent dfe98a4 commit 160a85c

3 files changed

Lines changed: 59 additions & 8 deletions

File tree

doc/whats-new.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ Bug Fixes
4949
- Fix :py:meth:`Dataset.sortby` and :py:meth:`DataArray.sortby` placing NaN values
5050
at the beginning instead of the end when using ``ascending=False`` (:issue:`7358`).
5151
By `Kristian Kollsgård <https://github.com/kkollsga>`_.
52+
- Raise :py:class:`FileNotFoundError` instead of a confusing ``ValueError`` when
53+
:py:func:`open_dataset` is called with a non-existent local file path
54+
(:issue:`10896`).
55+
By `Kristian Kollsgård <https://github.com/kkollsga>`_.
5256

5357
Documentation
5458
~~~~~~~~~~~~~

xarray/backends/plugins.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33
import functools
44
import inspect
55
import itertools
6+
import os
67
import warnings
78
from collections.abc import Callable
89
from importlib.metadata import entry_points
910
from typing import TYPE_CHECKING, Any
1011

1112
from xarray.backends.common import BACKEND_ENTRYPOINTS, BackendEntrypoint
1213
from xarray.core.options import OPTIONS
13-
from xarray.core.utils import module_available
14+
from xarray.core.utils import is_remote_uri, module_available
1415

1516
if TYPE_CHECKING:
16-
import os
1717
from importlib.metadata import EntryPoint, EntryPoints
1818

1919
from xarray.backends.common import AbstractDataStore
@@ -209,6 +209,11 @@ def guess_engine(
209209
"https://docs.xarray.dev/en/stable/getting-started-guide/installing.html"
210210
)
211211

212+
if isinstance(store_spec, str | os.PathLike):
213+
store_spec_str = str(store_spec)
214+
if not is_remote_uri(store_spec_str) and not os.path.exists(store_spec_str):
215+
raise FileNotFoundError(f"No such file: '{store_spec_str}'")
216+
212217
raise ValueError(error_msg)
213218

214219

xarray/tests/test_plugins.py

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -188,24 +188,66 @@ def test_build_engines_sorted() -> None:
188188
"xarray.backends.plugins.list_engines",
189189
mock.MagicMock(return_value={"dummy": DummyBackendEntrypointArgs()}),
190190
)
191-
def test_no_matching_engine_found() -> None:
192-
with pytest.raises(ValueError, match=r"did not find a match in any"):
191+
def test_no_matching_engine_found(tmp_path) -> None:
192+
# Non-existent local file raises FileNotFoundError
193+
with pytest.raises(FileNotFoundError, match=r"No such file"):
193194
plugins.guess_engine("not-valid")
194195

196+
# Existing file with unrecognized extension raises ValueError
197+
existing_file = tmp_path / "test.unknown"
198+
existing_file.write_bytes(b"")
199+
with pytest.raises(ValueError, match=r"did not find a match in any"):
200+
plugins.guess_engine(str(existing_file))
201+
202+
# Existing file with recognized magic number raises ValueError
203+
nc_file = tmp_path / "foo.nc"
204+
nc_file.write_bytes(b"CDF\x01\x00\x00\x00\x00")
195205
with pytest.raises(ValueError, match=r"found the following matches with the input"):
196-
plugins.guess_engine("foo.nc")
206+
plugins.guess_engine(str(nc_file))
197207

198208

199209
@mock.patch(
200210
"xarray.backends.plugins.list_engines",
201211
mock.MagicMock(return_value={}),
202212
)
203-
def test_engines_not_installed() -> None:
204-
with pytest.raises(ValueError, match=r"xarray is unable to open"):
213+
def test_engines_not_installed(tmp_path) -> None:
214+
# Non-existent local file raises FileNotFoundError
215+
with pytest.raises(FileNotFoundError, match=r"No such file"):
205216
plugins.guess_engine("not-valid")
206217

218+
# Existing file with no matching engine raises ValueError
219+
existing_file = tmp_path / "test.unknown"
220+
existing_file.write_bytes(b"")
221+
with pytest.raises(ValueError, match=r"xarray is unable to open"):
222+
plugins.guess_engine(str(existing_file))
223+
224+
# Existing file with recognized magic number raises ValueError
225+
nc_file = tmp_path / "foo.nc"
226+
nc_file.write_bytes(b"CDF\x01\x00\x00\x00\x00")
207227
with pytest.raises(ValueError, match=r"found the following matches with the input"):
208-
plugins.guess_engine("foo.nc")
228+
plugins.guess_engine(str(nc_file))
229+
230+
231+
@mock.patch(
232+
"xarray.backends.plugins.list_engines",
233+
mock.MagicMock(return_value={"dummy": DummyBackendEntrypointArgs()}),
234+
)
235+
def test_guess_engine_file_not_found() -> None:
236+
# Non-existent local file path (string)
237+
with pytest.raises(
238+
FileNotFoundError, match=r"No such file: '/nonexistent/path.h5'"
239+
):
240+
plugins.guess_engine("/nonexistent/path.h5")
241+
242+
# Non-existent local file path (PathLike)
243+
from pathlib import Path
244+
245+
with pytest.raises(FileNotFoundError, match=r"No such file"):
246+
plugins.guess_engine(Path("/nonexistent/path.h5"))
247+
248+
# Remote URIs should not raise FileNotFoundError (raises ValueError instead)
249+
with pytest.raises(ValueError):
250+
plugins.guess_engine("https://example.com/missing.h5")
209251

210252

211253
@pytest.mark.parametrize("engine", common.BACKEND_ENTRYPOINTS.keys())

0 commit comments

Comments
 (0)