|
2 | 2 |
|
3 | 3 | import importlib.util |
4 | 4 | import json |
| 5 | +import warnings |
5 | 6 | from pathlib import Path |
6 | | -from typing import TYPE_CHECKING, Any, Literal, Self, TypeAlias |
| 7 | +from typing import TYPE_CHECKING, Any, Literal, Self, TypeAlias, get_args |
7 | 8 |
|
8 | 9 | from zarr.abc.store import ByteRequest, Store |
9 | 10 | from zarr.core.buffer import Buffer, default_buffer_prototype |
@@ -54,59 +55,88 @@ def __init__(self, store: Store, path: str = "") -> None: |
54 | 55 | def read_only(self) -> bool: |
55 | 56 | return self.store.read_only |
56 | 57 |
|
| 58 | + @classmethod |
| 59 | + async def _create_open_instance(cls, store: Store, path: str) -> Self: |
| 60 | + """Helper to create and return a StorePath instance.""" |
| 61 | + await store._ensure_open() |
| 62 | + return cls(store, path) |
| 63 | + |
57 | 64 | @classmethod |
58 | 65 | async def open(cls, store: Store, path: str, mode: AccessModeLiteral | None = None) -> Self: |
59 | 66 | """ |
60 | 67 | Open StorePath based on the provided mode. |
61 | 68 |
|
62 | | - * If the mode is 'w-' and the StorePath contains keys, raise a FileExistsError. |
63 | | - * If the mode is 'w', delete all keys nested within the StorePath |
64 | | - * If the mode is 'a', 'r', or 'r+', do nothing |
| 69 | + * If the mode is None, return an opened version of the store with no changes. |
| 70 | + * If the mode is 'r+', 'w-', 'w', or 'a' and the store is read-only, raise a ValueError. |
| 71 | + * If the mode is 'r' and the store is not read-only, return a copy of the store with read_only set to True. |
| 72 | + * If the mode is 'w-' and the store is not read-only and the StorePath contains keys, raise a FileExistsError. |
| 73 | + * If the mode is 'w' and the store is not read-only, delete all keys nested within the StorePath. |
65 | 74 |
|
66 | 75 | Parameters |
67 | 76 | ---------- |
68 | 77 | mode : AccessModeLiteral |
69 | 78 | The mode to use when initializing the store path. |
70 | 79 |
|
| 80 | + The accepted values are: |
| 81 | +
|
| 82 | + - ``'r'``: read only (must exist) |
| 83 | + - ``'r+'``: read/write (must exist) |
| 84 | + - ``'a'``: read/write (create if doesn't exist) |
| 85 | + - ``'w'``: read/write (overwrite if exists) |
| 86 | + - ``'w-'``: read/write (create if doesn't exist). |
| 87 | +
|
71 | 88 | Raises |
72 | 89 | ------ |
73 | 90 | FileExistsError |
74 | 91 | If the mode is 'w-' and the store path already exists. |
75 | | - ValueError |
76 | | - If the mode is not "r" and the store is read-only, or |
77 | | - if the mode is "r" and the store is not read-only. |
78 | 92 | """ |
79 | 93 |
|
80 | | - await store._ensure_open() |
81 | | - self = cls(store, path) |
82 | | - |
83 | 94 | # fastpath if mode is None |
84 | 95 | if mode is None: |
85 | | - return self |
| 96 | + return await cls._create_open_instance(store, path) |
86 | 97 |
|
87 | | - if store.read_only and mode != "r": |
88 | | - raise ValueError(f"Store is read-only but mode is '{mode}'") |
89 | | - if not store.read_only and mode == "r": |
90 | | - raise ValueError(f"Store is not read-only but mode is '{mode}'") |
| 98 | + if mode not in get_args(AccessModeLiteral): |
| 99 | + raise ValueError(f"Invalid mode: {mode}, expected one of {AccessModeLiteral}") |
91 | 100 |
|
| 101 | + if store.read_only: |
| 102 | + # Don't allow write operations on a read-only store |
| 103 | + if mode != "r": |
| 104 | + raise ValueError( |
| 105 | + f"Store is read-only but mode is '{mode}'. Create a writable store or use 'r' mode." |
| 106 | + ) |
| 107 | + self = await cls._create_open_instance(store, path) |
| 108 | + elif mode == "r": |
| 109 | + # Create read-only copy for read mode on writable store |
| 110 | + try: |
| 111 | + warnings.warn( |
| 112 | + "Store is not read-only but mode is 'r'. Creating a read-only copy. " |
| 113 | + "This behavior may change in the future with a more granular permissions model.", |
| 114 | + UserWarning, |
| 115 | + stacklevel=2, |
| 116 | + ) |
| 117 | + self = await cls._create_open_instance(store.with_read_only(True), path) |
| 118 | + except NotImplementedError as e: |
| 119 | + raise ValueError( |
| 120 | + "Store is not read-only but mode is 'r'. Unable to create a read-only copy of the store." |
| 121 | + ) from e |
| 122 | + else: |
| 123 | + # writable store and writable mode |
| 124 | + await store._ensure_open() |
| 125 | + self = await cls._create_open_instance(store, path) |
| 126 | + |
| 127 | + # Handle mode-specific operations |
92 | 128 | match mode: |
93 | 129 | case "w-": |
94 | 130 | if not await self.is_empty(): |
95 | | - msg = ( |
96 | | - f"{self} is not empty, but `mode` is set to 'w-'." |
97 | | - "Either remove the existing objects in storage," |
98 | | - "or set `mode` to a value that handles pre-existing objects" |
99 | | - "in storage, like `a` or `w`." |
| 131 | + raise FileExistsError( |
| 132 | + f"Cannot create '{path}' with mode 'w-' because it already contains data. " |
| 133 | + f"Use mode 'w' to overwrite or 'a' to append." |
100 | 134 | ) |
101 | | - raise FileExistsError(msg) |
102 | 135 | case "w": |
103 | 136 | await self.delete_dir() |
104 | 137 | case "a" | "r" | "r+": |
105 | 138 | # No init action |
106 | 139 | pass |
107 | | - case _: |
108 | | - raise ValueError(f"Invalid mode: {mode}") |
109 | | - |
110 | 140 | return self |
111 | 141 |
|
112 | 142 | async def get( |
|
0 commit comments