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..fc3544eb3d 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 result == [] + @pytest.mark.slow_hypothesis def test_zarr_hierarchy() -> None: