Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions changes/3296.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Restore preservation of leading slashes in `FsspecStore.path`. The previous fix in `#3924` ran `path` through `normalize_path`, which is intended for zarr keys and strips leading slashes — corrupting absolute filesystem paths (e.g. `/home/foo/data.zarr` became `home/foo/data.zarr`) for backends like `LocalFileSystem`. `FsspecStore` now strips trailing slashes only, which still resolves the original `path="/"` issue without breaking absolute-path callers.
4 changes: 2 additions & 2 deletions src/zarr/storage/_fsspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
)
from zarr.core.buffer import Buffer
from zarr.errors import ZarrUserWarning
from zarr.storage._utils import _join_paths, normalize_path
from zarr.storage._utils import _join_paths

if TYPE_CHECKING:
from collections.abc import AsyncIterator, Iterable
Expand Down Expand Up @@ -127,7 +127,7 @@ def __init__(
) -> None:
super().__init__(read_only=read_only)
self.fs = fs
self.path = normalize_path(path)
self.path = path.rstrip("/")
self.allowed_exceptions = allowed_exceptions

if not self.fs.async_impl:
Expand Down
49 changes: 37 additions & 12 deletions tests/test_store/test_fsspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
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:
Expand Down Expand Up @@ -291,21 +290,47 @@ def array_roundtrip(store: FsspecStore) -> None:
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.
@pytest.mark.parametrize(
("path", "expected"),
[
("", ""),
("/", ""),
("//", ""),
("foo", "foo"),
("foo/", "foo"),
# Leading slashes are preserved: `path` is an opaque string passed to the
# underlying fsspec backend, and for some backends (notably LocalFileSystem)
# a leading slash is semantically meaningful. The store only strips trailing
# slashes so that `_join_paths([self.path, key])` produces a single separator
# between the root and the key.
("/foo", "/foo"),
("/foo/", "/foo"),
("//foo//", "//foo"),
# An absolute filesystem path round-trips intact.
("/home/runner/data.zarr", "/home/runner/data.zarr"),
("/home/runner/data.zarr/", "/home/runner/data.zarr"),
],
)
def test_fsspec_store_path_normalization(path: str, expected: str) -> None:
"""`FsspecStore.path` strips trailing slashes only.

Regression test for two related bugs:

- https://github.com/zarr-developers/zarr-python/issues/3922 -- a caller
passing ``path="/"`` resulted in `_join_paths(["/", "key"])` producing
``"//key"``, which missed the underlying object on backends like
``ReferenceFileSystem``. ``"/"`` must collapse to ``""``.

- Titiler regression after #3924 -- normalizing ``path`` via
``normalize_path`` corrupted absolute filesystem paths by stripping the
leading slash, turning ``/home/runner/data.zarr`` into
``home/runner/data.zarr`` (a relative path that doesn't exist). Leading
slashes must be preserved.
"""
sync_fs = fsspec.filesystem("memory")
fs = _make_async(sync_fs)
store = FsspecStore(fs=fs, path=path)
assert store.path == normalize_path(path)
assert store.path == expected


@pytest.mark.skipif(
Expand Down
Loading