diff --git a/changes/3310.feature.rst b/changes/3310.feature.rst new file mode 100644 index 0000000000..b21d3219fc --- /dev/null +++ b/changes/3310.feature.rst @@ -0,0 +1 @@ +Add obstore implementation of delete_dir. diff --git a/src/zarr/storage/_obstore.py b/src/zarr/storage/_obstore.py index e1469a991e..a311442555 100644 --- a/src/zarr/storage/_obstore.py +++ b/src/zarr/storage/_obstore.py @@ -13,6 +13,7 @@ Store, SuffixByteRequest, ) +from zarr.core.common import concurrent_map from zarr.core.config import config if TYPE_CHECKING: @@ -196,6 +197,18 @@ async def delete(self, key: str) -> None: with contextlib.suppress(FileNotFoundError): await obs.delete_async(self.store, key) + async def delete_dir(self, prefix: str) -> None: + # docstring inherited + import obstore as obs + + self._check_writable() + if prefix != "" and not prefix.endswith("/"): + prefix += "/" + + metas = await obs.list(self.store, prefix).collect_async() + keys = [(m["path"],) for m in metas] + await concurrent_map(keys, self.delete, limit=config.get("async.concurrency")) + @property def supports_partial_writes(self) -> bool: # docstring inherited diff --git a/tests/test_store/test_object.py b/tests/test_store/test_object.py index d8b89e56b7..73681d54d4 100644 --- a/tests/test_store/test_object.py +++ b/tests/test_store/test_object.py @@ -90,6 +90,17 @@ async def test_store_getsize_prefix(self, store: ObjectStore) -> None: total_size = await store.getsize_prefix("c") assert total_size == len(buf) * 2 + async def test_store_delete(self, store: ObjectStore) -> None: + assert store.supports_deletes + buf = cpu.Buffer.from_bytes(b"\x01\x02\x03\x04") + await store.set("foo/1", buf) + await store.set("foo/2", buf) + await store.delete("foo/1") + assert not await store.exists("foo/1") + assert await store.exists("foo/2") + await store.delete_dir("foo") # FileNotFoundErrors are suppressed + assert not await store.exists("foo/2") + @pytest.mark.slow_hypothesis def test_zarr_hierarchy():