From cf46a0610828ee8fcf50dcf74aae7bd66f5c22c4 Mon Sep 17 00:00:00 2001 From: NIK-TIGER-BILL Date: Fri, 5 Jun 2026 00:50:55 +0000 Subject: [PATCH 1/2] fix: handle ObjectStore.list_dir when prefix-like object exists When an object keyed exactly like the prefix (e.g., "g/" when prefix is "g") exists, list_dir would raise ValueError or yield an empty string because _relativize_path expects the path to start with prefix + "/". This commit filters out entries that exactly match the prefix or are the prefix with a trailing slash, preventing both the error and the empty string yield. Fixes #4032 Signed-off-by: NIK-TIGER-BILL --- src/zarr/storage/_obstore.py | 9 ++++++++- tests/test_store/test_object.py | 10 ++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/zarr/storage/_obstore.py b/src/zarr/storage/_obstore.py index ffea523f9f..51ff13b851 100644 --- a/src/zarr/storage/_obstore.py +++ b/src/zarr/storage/_obstore.py @@ -267,7 +267,14 @@ async def _transform_list_dir( for path in chain( list_result["common_prefixes"], map(itemgetter("path"), list_result["objects"]) ): - yield _relativize_path(path=path, prefix=prefix) + # Skip entries that exactly match the prefix or are the prefix with a trailing slash. + # These represent the prefix itself rather than children. + if path == prefix or path == f"{prefix}/": + continue + relative = _relativize_path(path=path, prefix=prefix) + if relative == "": + continue + yield relative class _BoundedRequest(TypedDict): diff --git a/tests/test_store/test_object.py b/tests/test_store/test_object.py index 6a4b796639..005c8c0494 100644 --- a/tests/test_store/test_object.py +++ b/tests/test_store/test_object.py @@ -96,6 +96,16 @@ async def test_store_getsize_prefix(self, store: ObjectStore[LocalStore]) -> Non total_size = await store.getsize_prefix("c") assert total_size == len(buf) * 2 + async def test_list_dir_prefix_object(self, store: ObjectStore[LocalStore]) -> None: + """list_dir should not raise ValueError when an object keyed like the prefix exists.""" + buf = cpu.Buffer.from_bytes(b"\x00") + # Create an object whose key is exactly the prefix with a trailing slash. + await self.set(store, "g/", buf) + # list_dir("g") should not raise even though "g/" looks like the prefix itself. + result = [k async for k in store.list_dir("g")] + # The entry should not appear as an empty string or raise. + assert "" not in result + @pytest.mark.slow_hypothesis def test_zarr_hierarchy() -> None: From d4d9f6bbb0d59e29038f67e340ee3596cd7d3c05 Mon Sep 17 00:00:00 2001 From: NIK-TIGER-BILL Date: Sat, 6 Jun 2026 03:03:54 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20review=20comment=20?= =?UTF-8?q?=E2=80=94=20assert=20exact=20expected=20result=20in=20list=5Fdi?= =?UTF-8?q?r=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: NIK-TIGER-BILL --- tests/test_store/test_object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_store/test_object.py b/tests/test_store/test_object.py index 005c8c0494..fc3544eb3d 100644 --- a/tests/test_store/test_object.py +++ b/tests/test_store/test_object.py @@ -104,7 +104,7 @@ async def test_list_dir_prefix_object(self, store: ObjectStore[LocalStore]) -> N # list_dir("g") should not raise even though "g/" looks like the prefix itself. result = [k async for k in store.list_dir("g")] # The entry should not appear as an empty string or raise. - assert "" not in result + assert result == [] @pytest.mark.slow_hypothesis