From 90812f562033a00b33faefa9d7a27bd9e16823d4 Mon Sep 17 00:00:00 2001 From: Nathan Zimmerman Date: Wed, 29 Jan 2025 20:23:03 -0600 Subject: [PATCH 1/2] Use removeprefix rather than replace to avoid separator deletion --- changes/2778.bugfix.rst | 1 + src/zarr/storage/_fsspec.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changes/2778.bugfix.rst diff --git a/changes/2778.bugfix.rst b/changes/2778.bugfix.rst new file mode 100644 index 0000000000..2968c4441c --- /dev/null +++ b/changes/2778.bugfix.rst @@ -0,0 +1 @@ +Use removeprefix rather than replace when removing filename prefixes in `FsspecStore.list` \ No newline at end of file diff --git a/src/zarr/storage/_fsspec.py b/src/zarr/storage/_fsspec.py index c30c9b601b..92c14fcc76 100644 --- a/src/zarr/storage/_fsspec.py +++ b/src/zarr/storage/_fsspec.py @@ -341,7 +341,7 @@ async def set_partial_values( async def list(self) -> AsyncIterator[str]: # docstring inherited allfiles = await self.fs._find(self.path, detail=False, withdirs=False) - for onefile in (a.replace(self.path + "/", "") for a in allfiles): + for onefile in (a.removeprefix(self.path + "/") for a in allfiles): yield onefile async def list_dir(self, prefix: str) -> AsyncIterator[str]: From 45447afd8523b91c1163cab0f773468099f57363 Mon Sep 17 00:00:00 2001 From: Nathan Zimmerman Date: Thu, 30 Jan 2025 09:46:59 -0600 Subject: [PATCH 2/2] Test list behavior with empty paths --- src/zarr/testing/store.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/zarr/testing/store.py b/src/zarr/testing/store.py index 1fe544d292..00427f6a0e 100644 --- a/src/zarr/testing/store.py +++ b/src/zarr/testing/store.py @@ -400,6 +400,37 @@ async def test_list_prefix(self, store: S) -> None: expected = tuple(sorted(expected)) assert observed == expected + async def test_list_empty_path(self, store: S) -> None: + """ + Verify that list and list_prefix work correctly when path is an empty string, + i.e. no unwanted replacement occurs. + """ + data = self.buffer_cls.from_bytes(b"") + store_dict = { + "foo/bar/zarr.json": data, + "foo/bar/c/1": data, + "foo/baz/c/0": data, + } + await store._set_many(store_dict.items()) + + # Test list() + observed_list = await _collect_aiterator(store.list()) + observed_list_sorted = sorted(observed_list) + expected_list_sorted = sorted(store_dict.keys()) + assert observed_list_sorted == expected_list_sorted + + # Test list_prefix() with an empty prefix + observed_prefix_empty = await _collect_aiterator(store.list_prefix("")) + observed_prefix_empty_sorted = sorted(observed_prefix_empty) + expected_prefix_empty_sorted = sorted(store_dict.keys()) + assert observed_prefix_empty_sorted == expected_prefix_empty_sorted + + # Test list_prefix() with a non-empty prefix + observed_prefix = await _collect_aiterator(store.list_prefix("foo/bar/")) + observed_prefix_sorted = sorted(observed_prefix) + expected_prefix_sorted = sorted(k for k in store_dict if k.startswith("foo/bar/")) + assert observed_prefix_sorted == expected_prefix_sorted + async def test_list_dir(self, store: S) -> None: root = "foo" store_dict = {