From e23cde05b51d41d0eeba516a26f6eeb786686099 Mon Sep 17 00:00:00 2001 From: Leo Ji Date: Tue, 31 Mar 2026 13:57:56 +0000 Subject: [PATCH] fix: auto-open ZipStore in list(), list_dir() and exists() ZipStore._get() and ._set() already auto-open the zip file when called without a prior open(). Apply the same pattern to list(), list_dir(), and exists() so that all store methods behave consistently when the store has not been explicitly opened. Closes #3846 Made-with: Cursor --- changes/3846.bugfix.md | 1 + src/zarr/storage/_zip.py | 6 ++++++ tests/test_store/test_zip.py | 25 +++++++++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 changes/3846.bugfix.md diff --git a/changes/3846.bugfix.md b/changes/3846.bugfix.md new file mode 100644 index 0000000000..bfda1b1693 --- /dev/null +++ b/changes/3846.bugfix.md @@ -0,0 +1 @@ +Fix `ZipStore.list()`, `list_dir()`, and `exists()` to auto-open the zip file when called before `open()`, consistent with the existing behavior of `get()` and `set()`. diff --git a/src/zarr/storage/_zip.py b/src/zarr/storage/_zip.py index 72bf9e335a..c5c7cbf3e8 100644 --- a/src/zarr/storage/_zip.py +++ b/src/zarr/storage/_zip.py @@ -245,6 +245,8 @@ async def delete(self, key: str) -> None: async def exists(self, key: str) -> bool: # docstring inherited + if not self._is_open: + self._sync_open() with self._lock: try: self._zf.getinfo(key) @@ -255,6 +257,8 @@ async def exists(self, key: str) -> bool: async def list(self) -> AsyncIterator[str]: # docstring inherited + if not self._is_open: + self._sync_open() with self._lock: for key in self._zf.namelist(): yield key @@ -267,6 +271,8 @@ async def list_prefix(self, prefix: str) -> AsyncIterator[str]: async def list_dir(self, prefix: str) -> AsyncIterator[str]: # docstring inherited + if not self._is_open: + self._sync_open() prefix = prefix.rstrip("/") keys = self._zf.namelist() diff --git a/tests/test_store/test_zip.py b/tests/test_store/test_zip.py index 744ee82945..5975a6d2a0 100644 --- a/tests/test_store/test_zip.py +++ b/tests/test_store/test_zip.py @@ -139,6 +139,31 @@ def test_externally_zipped_store(self, tmp_path: Path) -> None: assert isinstance(group := zipped["foo"], Group) assert list(group.keys()) == list(group.keys()) + async def test_list_without_explicit_open(self, tmp_path: Path) -> None: + # ZipStore.list(), list_dir(), and exists() should auto-open + # the zip file just like _get() and _set() do. + zip_path = tmp_path / "data.zip" + zarr_path = tmp_path / "foo.zarr" + root = zarr.open_group(store=zarr_path, mode="w") + root["x"] = np.array([1, 2, 3]) + shutil.make_archive(str(zarr_path), "zip", zarr_path) + shutil.move(str(zarr_path) + ".zip", zip_path) + + store = ZipStore(zip_path, mode="r") + assert not store._is_open + + keys = [k async for k in store.list()] + assert len(keys) > 0 + + store2 = ZipStore(zip_path, mode="r") + assert not store2._is_open + assert await store2.exists(keys[0]) + + store3 = ZipStore(zip_path, mode="r") + assert not store3._is_open + dir_keys = [k async for k in store3.list_dir("")] + assert len(dir_keys) > 0 + async def test_move(self, tmp_path: Path) -> None: origin = tmp_path / "origin.zip" destination = tmp_path / "some_folder" / "destination.zip"