diff --git a/changes/2856.feature.md b/changes/2856.feature.md new file mode 100644 index 0000000000..8769639726 --- /dev/null +++ b/changes/2856.feature.md @@ -0,0 +1 @@ +Opening a string or Path path is redirected to ZipStore if the path has a .zip suffix. diff --git a/docs/quick-start.md b/docs/quick-start.md index 0bad4f2e34..d8dbab805a 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -148,11 +148,14 @@ z[:, :] = np.random.random((100, 100)) store.close() ``` -To open an existing array from a ZIP file: +To open an existing array from a ZIP file you can open it with the usual function or by explicitly opening the `ZipStore` first : ```python exec="true" session="quickstart" source="above" result="code" -# Open the ZipStore in read-only mode +# Using the convenience functions, opening in read-only mode +z = zarr.open_array("data/example-5.zip", mode='r') + +# Using a ZipStore in read-only mode store = zarr.storage.ZipStore("data/example-5.zip", read_only=True) z = zarr.open_array(store, mode='r') diff --git a/docs/user-guide/storage.md b/docs/user-guide/storage.md index d5f840ab4b..74b823d6ba 100644 --- a/docs/user-guide/storage.md +++ b/docs/user-guide/storage.md @@ -47,7 +47,7 @@ print(group) `StoreLike` values can be: - a `Path` or string indicating a location on the local file system. - This will create a [local store](#local-store): + This will create a [local store](#local-store), unless the file name endswith the ".zip" suffix, in which case it creates a [zip store](#zip-store): ```python exec="true" session="storage" source="above" result="ansi" group = zarr.open_group(store='data/foo/bar') print(group) @@ -57,6 +57,10 @@ print(group) group = zarr.open_group(store=Path('data/foo/bar')) print(group) ``` + ```python exec="true" session="storage" source="above" result="ansi" + group = zarr.open_group(Path('data/foo.zip'), mode='w') + print(group) + ``` - an FSSpec URI string, indicating a [remote store](#remote-store) location: ```python exec="true" session="storage" source="above" result="ansi" diff --git a/src/zarr/storage/_common.py b/src/zarr/storage/_common.py index 1e13a9ac3f..5aa3239526 100644 --- a/src/zarr/storage/_common.py +++ b/src/zarr/storage/_common.py @@ -26,6 +26,7 @@ from zarr.storage._local import LocalStore from zarr.storage._memory import ManagedMemoryStore, MemoryStore from zarr.storage._utils import _join_paths, normalize_path, parse_store_url +from zarr.storage._zip import ZipStore _has_fsspec = importlib.util.find_spec("fsspec") if _has_fsspec: @@ -318,7 +319,8 @@ async def make_store( `StoreLike` objects are converted to `Store` as follows: - `Store` or `StorePath` = `Store` object. - - `Path` or `str` = `LocalStore` object. + - `Path` or `str` with a .zip suffix = `ZipStore` object. + - other `Path` or `str` = `LocalStore` object. - `str` that starts with a protocol = `FsspecStore` object. - `dict[str, Buffer]` = `MemoryStore` object. - `None` = `MemoryStore` object. @@ -383,6 +385,10 @@ async def make_store( # Create a new in-memory store return await make_store({}, mode=mode, storage_options=storage_options) + elif isinstance(store_like, Path) and store_like.suffix == ".zip": + # Create a new LocalStore + return await ZipStore.open(path=store_like, mode=mode, read_only=_read_only) + elif isinstance(store_like, Path): # Create a new LocalStore return await LocalStore.open(root=store_like, mode=mode, read_only=_read_only) diff --git a/tests/test_store/test_core.py b/tests/test_store/test_core.py index f2c81b87f9..d5cde0a02a 100644 --- a/tests/test_store/test_core.py +++ b/tests/test_store/test_core.py @@ -3,11 +3,12 @@ from pathlib import Path from typing import Any, Literal +import numpy as np import pytest from _pytest.compat import LEGACY_PATH import zarr -from zarr import Group +from zarr import Group, open_group from zarr.core.buffer import cpu from zarr.core.common import ZARR_JSON, AccessModeLiteral, ZarrFormat from zarr.storage import FsspecStore, LocalStore, MemoryStore, StoreLike, StorePath, ZipStore @@ -179,6 +180,36 @@ async def test_make_store_path_local( assert store_path.read_only == (mode == "r") +@pytest.mark.parametrize("store_type", [str, Path]) +@pytest.mark.parametrize("mode", ["r", "w"]) +async def test_make_store_path_zip_path( + tmpdir: LEGACY_PATH, + store_type: type[str] | type[Path] | type[LocalStore], + mode: AccessModeLiteral, +) -> None: + """ + Test that make_store_path creates a ZipStore given a path ending in .zip + """ + zippath = Path(tmpdir) / "zarr.zip" + store_like = store_type(str(zippath)) + + if mode == "r": + store = ZipStore(zippath, mode="w") + root = open_group(store=store, mode="w") + data = np.arange(10000, dtype=np.uint16).reshape(100, 100) + z = root.create_array( + shape=data.shape, chunks=(10, 10), name="foo", dtype=np.uint16, fill_value=99 + ) + z[:] = data + store.close() + + store_path = await make_store_path(store_like, mode=mode) + assert isinstance(store_path.store, ZipStore) + assert Path(store_path.store.path) == zippath + assert store_path.path == normalize_path("") + assert store_path.read_only == (mode == "r") + + @pytest.mark.parametrize("path", [None, "", "bar"]) @pytest.mark.parametrize("mode", ["r", "w"]) async def test_make_store_path_store_path(