From 6f52bfb1f2abc92e9bca74c2044ae7dfbe4b2876 Mon Sep 17 00:00:00 2001 From: Max Jones <14077947+maxrjones@users.noreply.github.com> Date: Thu, 19 Jun 2025 12:16:59 -0400 Subject: [PATCH 01/14] Create read only copy if needed when opening a store path --- src/zarr/storage/_common.py | 78 ++++++++++++++++++++++++----------- tests/test_api.py | 2 +- tests/test_store/test_core.py | 7 +++- 3 files changed, 60 insertions(+), 27 deletions(-) diff --git a/src/zarr/storage/_common.py b/src/zarr/storage/_common.py index f264728cf2..254d7129e9 100644 --- a/src/zarr/storage/_common.py +++ b/src/zarr/storage/_common.py @@ -2,8 +2,9 @@ import importlib.util import json +import warnings from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, Self, TypeAlias +from typing import TYPE_CHECKING, Any, Literal, Self, TypeAlias, get_args from zarr.abc.store import ByteRequest, Store from zarr.core.buffer import Buffer, default_buffer_prototype @@ -54,59 +55,88 @@ def __init__(self, store: Store, path: str = "") -> None: def read_only(self) -> bool: return self.store.read_only + @classmethod + async def _create_open_instance(cls, store: Store, path: str) -> Self: + """Helper to create and return a StorePath instance.""" + await store._ensure_open() + return cls(store, path) + @classmethod async def open(cls, store: Store, path: str, mode: AccessModeLiteral | None = None) -> Self: """ Open StorePath based on the provided mode. - * If the mode is 'w-' and the StorePath contains keys, raise a FileExistsError. - * If the mode is 'w', delete all keys nested within the StorePath - * If the mode is 'a', 'r', or 'r+', do nothing + * If the mode is None, return an opened version of the store with no changes. + * If the mode is 'r+', 'w-', 'w', or 'a' and the store is read-only, raise a ValueError. + * If the mode is 'r' and the store is not read-only, return a copy of the store with read_only set to True. + * If the mode is 'w-' and the store is not read-only and the StorePath contains keys, raise a FileExistsError. + * If the mode is 'w' and the store is not read-only, delete all keys nested within the StorePath. Parameters ---------- mode : AccessModeLiteral The mode to use when initializing the store path. + The accepted values are: + + - ``'r'``: read only (must exist) + - ``'r+'``: read/write (must exist) + - ``'a'``: read/write (create if doesn't exist) + - ``'w'``: read/write (overwrite if exists) + - ``'w-'``: read/write (create if doesn't exist). + Raises ------ FileExistsError If the mode is 'w-' and the store path already exists. - ValueError - If the mode is not "r" and the store is read-only, or - if the mode is "r" and the store is not read-only. """ - await store._ensure_open() - self = cls(store, path) - # fastpath if mode is None if mode is None: - return self + return await cls._create_open_instance(store, path) - if store.read_only and mode != "r": - raise ValueError(f"Store is read-only but mode is '{mode}'") - if not store.read_only and mode == "r": - raise ValueError(f"Store is not read-only but mode is '{mode}'") + if mode not in get_args(AccessModeLiteral): + raise ValueError(f"Invalid mode: {mode}, expected one of {AccessModeLiteral}") + if store.read_only: + # Don't allow write operations on a read-only store + if mode != "r": + raise ValueError( + f"Store is read-only but mode is '{mode}'. Create a writable store or use 'r' mode." + ) + self = await cls._create_open_instance(store, path) + elif mode == "r": + # Create read-only copy for read mode on writable store + try: + warnings.warn( + "Store is not read-only but mode is 'r'. Creating a read-only copy. " + "This behavior may change in the future with a more granular permissions model.", + UserWarning, + stacklevel=2, + ) + self = await cls._create_open_instance(store.with_read_only(True), path) + except NotImplementedError as e: + raise ValueError( + "Store is not read-only but mode is 'r'. Unable to create a read-only copy of the store." + ) from e + else: + # writable store and writable mode + await store._ensure_open() + self = await cls._create_open_instance(store, path) + + # Handle mode-specific operations match mode: case "w-": if not await self.is_empty(): - msg = ( - f"{self} is not empty, but `mode` is set to 'w-'." - "Either remove the existing objects in storage," - "or set `mode` to a value that handles pre-existing objects" - "in storage, like `a` or `w`." + raise FileExistsError( + f"Cannot create '{path}' with mode 'w-' because it already contains data. " + f"Use mode 'w' to overwrite or 'a' to append." ) - raise FileExistsError(msg) case "w": await self.delete_dir() case "a" | "r" | "r+": # No init action pass - case _: - raise ValueError(f"Invalid mode: {mode}") - return self async def get( diff --git a/tests/test_api.py b/tests/test_api.py index 2a95d7b97c..e6cb612a82 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1318,7 +1318,7 @@ def test_no_overwrite_open(tmp_path: Path, open_func: Callable, mode: str) -> No existing_fpath = add_empty_file(tmp_path) assert existing_fpath.exists() - with contextlib.suppress(FileExistsError, FileNotFoundError, ValueError): + with contextlib.suppress(FileExistsError, FileNotFoundError, UserWarning): open_func(store=store, mode=mode) if mode == "w": assert not existing_fpath.exists() diff --git a/tests/test_store/test_core.py b/tests/test_store/test_core.py index e9c9319ad3..efbbee146a 100644 --- a/tests/test_store/test_core.py +++ b/tests/test_store/test_core.py @@ -263,8 +263,11 @@ def test_relativize_path_invalid() -> None: _relativize_path(path="a/b/c", prefix="b") -def test_invalid_open_mode() -> None: +def test_different_open_mode() -> None: store = MemoryStore() zarr.create((100,), store=store, zarr_format=2, path="a") - with pytest.raises(ValueError, match="Store is not read-only but mode is 'r'"): + with pytest.warns( + UserWarning, + match="Store is not read-only but mode is 'r'. Attempting to create a read-only copy. This behavior may change in the future with a more granular permissions model.", + ): zarr.open_array(store=store, path="a", zarr_format=2, mode="r") From 737ff9b967c11fef1e1bdf90ff7c7cb2b8a35a54 Mon Sep 17 00:00:00 2001 From: Max Jones <14077947+maxrjones@users.noreply.github.com> Date: Thu, 19 Jun 2025 12:22:18 -0400 Subject: [PATCH 02/14] Add ValueError to Raises section --- src/zarr/storage/_common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/zarr/storage/_common.py b/src/zarr/storage/_common.py index 254d7129e9..18b6ff1adb 100644 --- a/src/zarr/storage/_common.py +++ b/src/zarr/storage/_common.py @@ -89,6 +89,8 @@ async def open(cls, store: Store, path: str, mode: AccessModeLiteral | None = No ------ FileExistsError If the mode is 'w-' and the store path already exists. + ValueError + If the mode is not "r" and the store is read-only, or """ # fastpath if mode is None From 957cd64308998e7569605815cf3472dfb0afb548 Mon Sep 17 00:00:00 2001 From: Max Jones <14077947+maxrjones@users.noreply.github.com> Date: Thu, 19 Jun 2025 12:32:28 -0400 Subject: [PATCH 03/14] Update expected warning --- tests/test_store/test_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_store/test_core.py b/tests/test_store/test_core.py index efbbee146a..1cb773267c 100644 --- a/tests/test_store/test_core.py +++ b/tests/test_store/test_core.py @@ -268,6 +268,6 @@ def test_different_open_mode() -> None: zarr.create((100,), store=store, zarr_format=2, path="a") with pytest.warns( UserWarning, - match="Store is not read-only but mode is 'r'. Attempting to create a read-only copy. This behavior may change in the future with a more granular permissions model.", + match="Store is not read-only but mode is 'r'. Creating a read-only copy. This behavior may change in the future with a more granular permissions model", ): zarr.open_array(store=store, path="a", zarr_format=2, mode="r") From 23f43762a9282f5160e15056b13d2fa03a684c26 Mon Sep 17 00:00:00 2001 From: Max Jones <14077947+maxrjones@users.noreply.github.com> Date: Thu, 19 Jun 2025 13:19:52 -0400 Subject: [PATCH 04/14] Update src/zarr/storage/_common.py Co-authored-by: Davis Bennett --- src/zarr/storage/_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zarr/storage/_common.py b/src/zarr/storage/_common.py index 18b6ff1adb..2503aa0b73 100644 --- a/src/zarr/storage/_common.py +++ b/src/zarr/storage/_common.py @@ -104,7 +104,7 @@ async def open(cls, store: Store, path: str, mode: AccessModeLiteral | None = No # Don't allow write operations on a read-only store if mode != "r": raise ValueError( - f"Store is read-only but mode is '{mode}'. Create a writable store or use 'r' mode." + f"Store is read-only but mode is {mode!r}. Create a writable store or use 'r' mode." ) self = await cls._create_open_instance(store, path) elif mode == "r": From a42dc7cd9e552fb82a2be8d90852057188c68b35 Mon Sep 17 00:00:00 2001 From: Max Jones <14077947+maxrjones@users.noreply.github.com> Date: Thu, 19 Jun 2025 13:37:05 -0400 Subject: [PATCH 05/14] Use ANY_ACCESS_MODE --- src/zarr/core/common.py | 2 ++ src/zarr/storage/_common.py | 15 +++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/zarr/core/common.py b/src/zarr/core/common.py index 2ba5914ea5..13ee8bc5a0 100644 --- a/src/zarr/core/common.py +++ b/src/zarr/core/common.py @@ -10,6 +10,7 @@ from typing import ( TYPE_CHECKING, Any, + Final, Generic, Literal, TypedDict, @@ -39,6 +40,7 @@ JSON = str | int | float | Mapping[str, "JSON"] | Sequence["JSON"] | None MemoryOrder = Literal["C", "F"] AccessModeLiteral = Literal["r", "r+", "a", "w", "w-"] +ANY_ACCESS_MODE: Final = "r", "r+", "a", "w", "w-" DimensionNames = Iterable[str | None] | None TName = TypeVar("TName", bound=str) diff --git a/src/zarr/storage/_common.py b/src/zarr/storage/_common.py index 2503aa0b73..600f3065e4 100644 --- a/src/zarr/storage/_common.py +++ b/src/zarr/storage/_common.py @@ -4,11 +4,18 @@ import json import warnings from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, Self, TypeAlias, get_args +from typing import TYPE_CHECKING, Any, Literal, Self, TypeAlias from zarr.abc.store import ByteRequest, Store from zarr.core.buffer import Buffer, default_buffer_prototype -from zarr.core.common import ZARR_JSON, ZARRAY_JSON, ZGROUP_JSON, AccessModeLiteral, ZarrFormat +from zarr.core.common import ( + ANY_ACCESS_MODE, + ZARR_JSON, + ZARRAY_JSON, + ZGROUP_JSON, + AccessModeLiteral, + ZarrFormat, +) from zarr.errors import ContainsArrayAndGroupError, ContainsArrayError, ContainsGroupError from zarr.storage._local import LocalStore from zarr.storage._memory import MemoryStore @@ -97,8 +104,8 @@ async def open(cls, store: Store, path: str, mode: AccessModeLiteral | None = No if mode is None: return await cls._create_open_instance(store, path) - if mode not in get_args(AccessModeLiteral): - raise ValueError(f"Invalid mode: {mode}, expected one of {AccessModeLiteral}") + if mode not in ANY_ACCESS_MODE: + raise ValueError(f"Invalid mode: {mode}, expected one of {ANY_ACCESS_MODE}") if store.read_only: # Don't allow write operations on a read-only store From aba491915f7bb6b06891dfc4f9285e31d60ae4d3 Mon Sep 17 00:00:00 2001 From: Max Jones <14077947+maxrjones@users.noreply.github.com> Date: Fri, 20 Jun 2025 08:21:02 -0400 Subject: [PATCH 06/14] Update src/zarr/storage/_common.py Co-authored-by: David Stansby --- src/zarr/storage/_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zarr/storage/_common.py b/src/zarr/storage/_common.py index 600f3065e4..958cc8c5a5 100644 --- a/src/zarr/storage/_common.py +++ b/src/zarr/storage/_common.py @@ -121,7 +121,7 @@ async def open(cls, store: Store, path: str, mode: AccessModeLiteral | None = No "Store is not read-only but mode is 'r'. Creating a read-only copy. " "This behavior may change in the future with a more granular permissions model.", UserWarning, - stacklevel=2, + stacklevel=1, ) self = await cls._create_open_instance(store.with_read_only(True), path) except NotImplementedError as e: From 0129458c3ae955aee2e10a06444fdab905402c51 Mon Sep 17 00:00:00 2001 From: Max Jones <14077947+maxrjones@users.noreply.github.com> Date: Fri, 20 Jun 2025 08:21:08 -0400 Subject: [PATCH 07/14] Update src/zarr/storage/_common.py Co-authored-by: David Stansby --- src/zarr/storage/_common.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/zarr/storage/_common.py b/src/zarr/storage/_common.py index 958cc8c5a5..92528da57a 100644 --- a/src/zarr/storage/_common.py +++ b/src/zarr/storage/_common.py @@ -143,9 +143,6 @@ async def open(cls, store: Store, path: str, mode: AccessModeLiteral | None = No ) case "w": await self.delete_dir() - case "a" | "r" | "r+": - # No init action - pass return self async def get( From 10e50e45524bf330cd4b3daad2b2bdb1f515c52e Mon Sep 17 00:00:00 2001 From: Max Jones <14077947+maxrjones@users.noreply.github.com> Date: Fri, 20 Jun 2025 08:25:27 -0400 Subject: [PATCH 08/14] Update changes --- changes/3068.bugfix.rst | 1 - changes/3156.bugfix.rst | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 changes/3068.bugfix.rst create mode 100644 changes/3156.bugfix.rst diff --git a/changes/3068.bugfix.rst b/changes/3068.bugfix.rst deleted file mode 100644 index 9ada322c13..0000000000 --- a/changes/3068.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Trying to open an array with ``mode='r'`` when the store is not read-only now raises an error. diff --git a/changes/3156.bugfix.rst b/changes/3156.bugfix.rst new file mode 100644 index 0000000000..64218b6707 --- /dev/null +++ b/changes/3156.bugfix.rst @@ -0,0 +1 @@ +Trying to open a StorePath/Array with ``mode='r'`` when the store is not read-only creates a read-only copy of the store. From 7ad760fba89e77a3ec5ad9649ada3a99799feeb6 Mon Sep 17 00:00:00 2001 From: Max Jones <14077947+maxrjones@users.noreply.github.com> Date: Fri, 20 Jun 2025 08:33:38 -0400 Subject: [PATCH 09/14] Try using get_args on definition --- src/zarr/core/common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/zarr/core/common.py b/src/zarr/core/common.py index 13ee8bc5a0..b1b5849bbe 100644 --- a/src/zarr/core/common.py +++ b/src/zarr/core/common.py @@ -16,6 +16,7 @@ TypedDict, TypeVar, cast, + get_args, overload, ) @@ -40,7 +41,7 @@ JSON = str | int | float | Mapping[str, "JSON"] | Sequence["JSON"] | None MemoryOrder = Literal["C", "F"] AccessModeLiteral = Literal["r", "r+", "a", "w", "w-"] -ANY_ACCESS_MODE: Final = "r", "r+", "a", "w", "w-" +ANY_ACCESS_MODE: Final = get_args(AccessModeLiteral) DimensionNames = Iterable[str | None] | None TName = TypeVar("TName", bound=str) From a1949f1044ae8867e7ec1af8631d459bdce4df81 Mon Sep 17 00:00:00 2001 From: Max Jones <14077947+maxrjones@users.noreply.github.com> Date: Fri, 20 Jun 2025 09:37:49 -0400 Subject: [PATCH 10/14] Revert "Try using get_args on definition" This reverts commit 7ad760fba89e77a3ec5ad9649ada3a99799feeb6. --- src/zarr/core/common.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/zarr/core/common.py b/src/zarr/core/common.py index b1b5849bbe..13ee8bc5a0 100644 --- a/src/zarr/core/common.py +++ b/src/zarr/core/common.py @@ -16,7 +16,6 @@ TypedDict, TypeVar, cast, - get_args, overload, ) @@ -41,7 +40,7 @@ JSON = str | int | float | Mapping[str, "JSON"] | Sequence["JSON"] | None MemoryOrder = Literal["C", "F"] AccessModeLiteral = Literal["r", "r+", "a", "w", "w-"] -ANY_ACCESS_MODE: Final = get_args(AccessModeLiteral) +ANY_ACCESS_MODE: Final = "r", "r+", "a", "w", "w-" DimensionNames = Iterable[str | None] | None TName = TypeVar("TName", bound=str) From 202d60689fe900537f40fcced8365edf41793805 Mon Sep 17 00:00:00 2001 From: Max Jones <14077947+maxrjones@users.noreply.github.com> Date: Fri, 20 Jun 2025 09:50:01 -0400 Subject: [PATCH 11/14] Add test --- tests/test_common.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/tests/test_common.py b/tests/test_common.py index c28723d1a8..0944c3375a 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,23 +1,36 @@ from __future__ import annotations -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Iterable - from typing import Any, Literal +from typing import TYPE_CHECKING, get_args import numpy as np import pytest -from zarr.core.common import parse_name, parse_shapelike, product +from zarr.core.common import ( + ANY_ACCESS_MODE, + AccessModeLiteral, + parse_name, + parse_shapelike, + product, +) from zarr.core.config import parse_indexing_order +if TYPE_CHECKING: + from collections.abc import Iterable + from typing import Any, Literal + @pytest.mark.parametrize("data", [(0, 0, 0, 0), (1, 3, 4, 5, 6), (2, 4)]) def test_product(data: tuple[int, ...]) -> None: assert product(data) == np.prod(data) +def test_access_modes() -> None: + """ + Test that the access modes type and variable for run-time checking are equivalent. + """ + assert set(ANY_ACCESS_MODE) == set(get_args(AccessModeLiteral)) + + # todo: test def test_concurrent_map() -> None: ... From 4e099e4f8c49090b182cf1c60e25006446f26253 Mon Sep 17 00:00:00 2001 From: Max Jones <14077947+maxrjones@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:29:00 -0400 Subject: [PATCH 12/14] Remove warning --- src/zarr/storage/_common.py | 7 ------- tests/test_store/test_core.py | 6 +----- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/zarr/storage/_common.py b/src/zarr/storage/_common.py index 92528da57a..3fb1c73ef5 100644 --- a/src/zarr/storage/_common.py +++ b/src/zarr/storage/_common.py @@ -2,7 +2,6 @@ import importlib.util import json -import warnings from pathlib import Path from typing import TYPE_CHECKING, Any, Literal, Self, TypeAlias @@ -117,12 +116,6 @@ async def open(cls, store: Store, path: str, mode: AccessModeLiteral | None = No elif mode == "r": # Create read-only copy for read mode on writable store try: - warnings.warn( - "Store is not read-only but mode is 'r'. Creating a read-only copy. " - "This behavior may change in the future with a more granular permissions model.", - UserWarning, - stacklevel=1, - ) self = await cls._create_open_instance(store.with_read_only(True), path) except NotImplementedError as e: raise ValueError( diff --git a/tests/test_store/test_core.py b/tests/test_store/test_core.py index 1cb773267c..914424259a 100644 --- a/tests/test_store/test_core.py +++ b/tests/test_store/test_core.py @@ -266,8 +266,4 @@ def test_relativize_path_invalid() -> None: def test_different_open_mode() -> None: store = MemoryStore() zarr.create((100,), store=store, zarr_format=2, path="a") - with pytest.warns( - UserWarning, - match="Store is not read-only but mode is 'r'. Creating a read-only copy. This behavior may change in the future with a more granular permissions model", - ): - zarr.open_array(store=store, path="a", zarr_format=2, mode="r") + zarr.open_array(store=store, path="a", zarr_format=2, mode="r") From 2616b2f8d030e6adfede6202c88d5331b3992446 Mon Sep 17 00:00:00 2001 From: Max Jones <14077947+maxrjones@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:33:28 -0400 Subject: [PATCH 13/14] Apply suggestion for try; except shortening Co-authored-by: Tom Nicholas --- src/zarr/storage/_common.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/zarr/storage/_common.py b/src/zarr/storage/_common.py index 3fb1c73ef5..8726c50b2c 100644 --- a/src/zarr/storage/_common.py +++ b/src/zarr/storage/_common.py @@ -116,14 +116,15 @@ async def open(cls, store: Store, path: str, mode: AccessModeLiteral | None = No elif mode == "r": # Create read-only copy for read mode on writable store try: - self = await cls._create_open_instance(store.with_read_only(True), path) + read_only_store = store.with_read_only(True) except NotImplementedError as e: raise ValueError( - "Store is not read-only but mode is 'r'. Unable to create a read-only copy of the store." + "Store is not read-only but mode is 'r'. Unable to create a read-only copy of the store. " + "Please use a read-only store or a storage class that implements .with_read_only()" ) from e + self = await cls._create_open_instance(read_only_store, path) else: # writable store and writable mode - await store._ensure_open() self = await cls._create_open_instance(store, path) # Handle mode-specific operations From 07bb607d566e468ec8cd04bb8e8579d221539743 Mon Sep 17 00:00:00 2001 From: Max Jones <14077947+maxrjones@users.noreply.github.com> Date: Tue, 24 Jun 2025 17:10:42 -0400 Subject: [PATCH 14/14] Improve code coverage --- src/zarr/storage/_common.py | 2 +- tests/test_store/test_core.py | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/zarr/storage/_common.py b/src/zarr/storage/_common.py index 8726c50b2c..e25fa28424 100644 --- a/src/zarr/storage/_common.py +++ b/src/zarr/storage/_common.py @@ -120,7 +120,7 @@ async def open(cls, store: Store, path: str, mode: AccessModeLiteral | None = No except NotImplementedError as e: raise ValueError( "Store is not read-only but mode is 'r'. Unable to create a read-only copy of the store. " - "Please use a read-only store or a storage class that implements .with_read_only()" + "Please use a read-only store or a storage class that implements .with_read_only()." ) from e self = await cls._create_open_instance(read_only_store, path) else: diff --git a/tests/test_store/test_core.py b/tests/test_store/test_core.py index 914424259a..a3850de90f 100644 --- a/tests/test_store/test_core.py +++ b/tests/test_store/test_core.py @@ -7,7 +7,7 @@ import zarr from zarr import Group from zarr.core.common import AccessModeLiteral, ZarrFormat -from zarr.storage import FsspecStore, LocalStore, MemoryStore, StoreLike, StorePath +from zarr.storage import FsspecStore, LocalStore, MemoryStore, StoreLike, StorePath, ZipStore from zarr.storage._common import contains_array, contains_group, make_store_path from zarr.storage._utils import ( _join_paths, @@ -263,7 +263,19 @@ def test_relativize_path_invalid() -> None: _relativize_path(path="a/b/c", prefix="b") -def test_different_open_mode() -> None: +def test_different_open_mode(tmp_path: LEGACY_PATH) -> None: + # Test with a store that implements .with_read_only() store = MemoryStore() zarr.create((100,), store=store, zarr_format=2, path="a") - zarr.open_array(store=store, path="a", zarr_format=2, mode="r") + arr = zarr.open_array(store=store, path="a", zarr_format=2, mode="r") + assert arr.store.read_only + + # Test with a store that doesn't implement .with_read_only() + zarr_path = tmp_path / "foo.zarr" + store = ZipStore(zarr_path, mode="w") + zarr.create((100,), store=store, zarr_format=2, path="a") + with pytest.raises( + ValueError, + match="Store is not read-only but mode is 'r'. Unable to create a read-only copy of the store. Please use a read-only store or a storage class that implements .with_read_only().", + ): + zarr.open_array(store=store, path="a", zarr_format=2, mode="r")