Skip to content
Open
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
5 changes: 5 additions & 0 deletions doc/whats-new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ Deprecations

Bug Fixes
~~~~~~~~~
- :py:func:`open_dataset` can detect local Zarr directories on Windows without
raising ``PermissionError`` while checking file-based backends. Permission
errors for ordinary files still propagate (:issue:`11799`, :pull:`11802`).
By `Gabriel Demetrios Lafis <https://github.com/galafis>`_.

- :py:meth:`Dataset.query` now resolves coordinates in datasets with no data
variables (:issue:`6449`).
By `Liang, Jingyi <https://github.com/LJimmy007>`_.
Expand Down
8 changes: 7 additions & 1 deletion xarray/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -777,7 +777,7 @@ def read_magic_number_from_file(filename_or_obj, count=8) -> bytes:
return magic_number


def try_read_magic_number_from_path(pathlike, count=8) -> bytes | None:
def try_read_magic_number_from_path(pathlike: Any, count: int = 8) -> bytes | None:
if isinstance(pathlike, str) or hasattr(pathlike, "__fspath__"):
path = os.fspath(pathlike)
try:
Expand All @@ -788,6 +788,12 @@ def try_read_magic_number_from_path(pathlike, count=8) -> bytes | None:
return read_magic_number_from_file(f, count)
except (FileNotFoundError, IsADirectoryError, TypeError):
pass
except PermissionError:
# Windows reports PermissionError rather than IsADirectoryError
# when opening a directory. Let other backends inspect it, but
# preserve permission errors for ordinary files.
if not os.path.isdir(path):
raise
return None


Expand Down
18 changes: 18 additions & 0 deletions xarray/tests/backends/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import sys
from importlib.metadata import EntryPoint, EntryPoints
from itertools import starmap
from pathlib import Path
from unittest import mock

import pytest
Expand All @@ -26,6 +27,23 @@
importlib_metadata_mock = "importlib.metadata"


@requires_netCDF4
@requires_zarr
@pytest.mark.parametrize("zarr_format", [2, 3])
@pytest.mark.parametrize("path_as_string", [False, True])
def test_guess_engine_local_zarr_directory(
tmp_path: Path, zarr_format: int, path_as_string: bool
) -> None:
expected = xr.Dataset({"temperature": ("time", [1.0, 2.0, 3.0])})
path = tmp_path / "example.zarr"
expected.to_zarr(path, zarr_format=zarr_format, consolidated=False)
store = str(path) if path_as_string else path

assert plugins.guess_engine(store) == "zarr"
with xr.open_dataset(store, consolidated=False) as actual:
xr.testing.assert_identical(actual, expected)


class DummyBackendEntrypointArgs(common.BackendEntrypoint):
def open_dataset(filename_or_obj, *args): # type: ignore[override]
pass
Expand Down
26 changes: 26 additions & 0 deletions xarray/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,32 @@ def test_try_read_magic_number_from_path_missing(tmp_path: Path) -> None:
assert try_read_magic_number_from_path(tmp_path / "missing.nc") is None


def test_try_read_magic_number_from_directory(tmp_path: Path) -> None:
assert try_read_magic_number_from_path(tmp_path) is None
assert try_read_magic_number_from_path(str(tmp_path)) is None


@pytest.mark.parametrize("directory", [False, True])
def test_try_read_magic_number_permission_error(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, directory: bool
) -> None:
path = tmp_path / "store"
if directory:
path.mkdir()
else:
path.write_bytes(b"CDF")

def denied_open(*args: Any, **kwargs: Any) -> IO[Any]:
raise PermissionError("Access denied")

monkeypatch.setattr(utils, "open", denied_open, raising=False)
if directory:
assert try_read_magic_number_from_path(path) is None
else:
with pytest.raises(PermissionError, match="Access denied"):
try_read_magic_number_from_path(path)


def test_try_read_magic_number_from_path_unbuffered(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
Expand Down
Loading