diff --git a/changes/3924.bugfix.md b/changes/3924.bugfix.md new file mode 100644 index 0000000000..54382d775e --- /dev/null +++ b/changes/3924.bugfix.md @@ -0,0 +1 @@ +Apply path normalization to the `path` attribute of `FsspecStore`, ensuring that leading and trailing "/" symbols are removed. diff --git a/src/zarr/storage/_fsspec.py b/src/zarr/storage/_fsspec.py index ac50a0527f..74e5869a66 100644 --- a/src/zarr/storage/_fsspec.py +++ b/src/zarr/storage/_fsspec.py @@ -16,7 +16,7 @@ ) from zarr.core.buffer import Buffer from zarr.errors import ZarrUserWarning -from zarr.storage._utils import _join_paths +from zarr.storage._utils import _join_paths, normalize_path if TYPE_CHECKING: from collections.abc import AsyncIterator, Iterable @@ -127,7 +127,7 @@ def __init__( ) -> None: super().__init__(read_only=read_only) self.fs = fs - self.path = path + self.path = normalize_path(path) self.allowed_exceptions = allowed_exceptions if not self.fs.async_impl: diff --git a/tests/test_store/test_fsspec.py b/tests/test_store/test_fsspec.py index 5e9e33f0e4..00b989a5de 100644 --- a/tests/test_store/test_fsspec.py +++ b/tests/test_store/test_fsspec.py @@ -17,6 +17,7 @@ from zarr.errors import ZarrUserWarning from zarr.storage import FsspecStore from zarr.storage._fsspec import _make_async +from zarr.storage._utils import normalize_path from zarr.testing.store import StoreTests if TYPE_CHECKING: @@ -286,6 +287,27 @@ def array_roundtrip(store: FsspecStore) -> None: np.testing.assert_array_equal(arr[:], data) +@pytest.mark.skipif( + parse_version(fsspec.__version__) < parse_version("2024.12.0"), + reason="No AsyncFileSystemWrapper", +) +@pytest.mark.parametrize("path", ["", "/", "//", "foo", "foo/", "/foo", "/foo/", "//foo//"]) +def test_fsspec_store_path_normalization(path: str) -> None: + """`FsspecStore.path` is normalized to the canonical form, matching + `normalize_path`, regardless of the surface representation the caller + supplies. + + Regression test for https://github.com/zarr-developers/zarr-python/issues/3922 + -- when a caller passed `path="/"` the leading slash flowed through + unmodified to subsequent `_join_paths([self.path, key])` calls, producing + `"//key"` and missing the underlying object. + """ + sync_fs = fsspec.filesystem("memory") + fs = _make_async(sync_fs) + store = FsspecStore(fs=fs, path=path) + assert store.path == normalize_path(path) + + @pytest.mark.skipif( parse_version(fsspec.__version__) < parse_version("2024.12.0"), reason="No AsyncFileSystemWrapper",