diff --git a/src/zarr/storage/_obstore.py b/src/zarr/storage/_obstore.py index 7ef0b40628..9568513d9b 100644 --- a/src/zarr/storage/_obstore.py +++ b/src/zarr/storage/_obstore.py @@ -260,7 +260,7 @@ async def _transform_list_dir( # We assume that the underlying object-store implementation correctly handles the # prefix, so we don't double-check that the returned results actually start with the # given prefix. - prefixes = [obj.lstrip(prefix).lstrip("/") for obj in list_result["common_prefixes"]] + prefixes = [obj.removeprefix(prefix).lstrip("/") for obj in list_result["common_prefixes"]] objects = [obj["path"].removeprefix(prefix).lstrip("/") for obj in list_result["objects"]] for item in prefixes + objects: yield item diff --git a/tests/test_store/test_object.py b/tests/test_store/test_object.py index 3217069c2d..756f6309e3 100644 --- a/tests/test_store/test_object.py +++ b/tests/test_store/test_object.py @@ -86,6 +86,24 @@ async def test_store_getsize_prefix(self, store: ObjectStore) -> None: assert total_size == len(buf) * 2 +async def test_list_dir_with_nested_prefix(tmpdir): + """Regression test for https://github.com/zarr-developers/zarr-python/issues/3753. + + list_dir must not corrupt directory names when prefix shares characters + with child names, which happened because lstrip strips characters not prefixes. + """ + store = ObjectStore(LocalStore(prefix=str(tmpdir))) + + buf = cpu.Buffer.from_bytes(b"\x01") + await store.set("subdir/data.zarr/zarr.json", buf) + await store.set("subdir/data.zarr/temp/zarr.json", buf) + await store.set("subdir/data.zarr/temp/c/0/0", buf) + + results = sorted([item async for item in store.list_dir("subdir/data.zarr")]) + assert "temp" in results, f"Expected 'temp' in {results}" + assert "zarr.json" in results + + @pytest.mark.slow_hypothesis def test_zarr_hierarchy(): sync_store = ObjectStore(MemoryStore())