Skip to content

Commit 9ace4fa

Browse files
committed
Merge branch 'main' of https://github.com/zarr-developers/zarr-python into refactor/store-mode
2 parents ed8a8ec + 693e11c commit 9ace4fa

File tree

7 files changed

+192
-27
lines changed

7 files changed

+192
-27
lines changed

src/zarr/abc/store.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from typing import TYPE_CHECKING, Protocol, runtime_checkable
77

88
if TYPE_CHECKING:
9-
from collections.abc import AsyncGenerator, Iterable
9+
from collections.abc import AsyncGenerator, AsyncIterator, Iterable
1010
from types import TracebackType
1111
from typing import Any, Self, TypeAlias
1212

@@ -263,16 +263,19 @@ def supports_listing(self) -> bool:
263263
...
264264

265265
@abstractmethod
266-
def list(self) -> AsyncGenerator[str]:
266+
def list(self) -> AsyncIterator[str]:
267267
"""Retrieve all keys in the store.
268268
269269
Returns
270270
-------
271-
AsyncGenerator[str, None]
271+
AsyncIterator[str]
272272
"""
273+
# This method should be async, like overridden methods in child classes.
274+
# However, that's not straightforward:
275+
# https://stackoverflow.com/questions/68905848
273276

274277
@abstractmethod
275-
def list_prefix(self, prefix: str) -> AsyncGenerator[str]:
278+
def list_prefix(self, prefix: str) -> AsyncIterator[str]:
276279
"""
277280
Retrieve all keys in the store that begin with a given prefix. Keys are returned relative
278281
to the root of the store.
@@ -283,11 +286,14 @@ def list_prefix(self, prefix: str) -> AsyncGenerator[str]:
283286
284287
Returns
285288
-------
286-
AsyncGenerator[str, None]
289+
AsyncIterator[str]
287290
"""
291+
# This method should be async, like overridden methods in child classes.
292+
# However, that's not straightforward:
293+
# https://stackoverflow.com/questions/68905848
288294

289295
@abstractmethod
290-
def list_dir(self, prefix: str) -> AsyncGenerator[str]:
296+
def list_dir(self, prefix: str) -> AsyncIterator[str]:
291297
"""
292298
Retrieve all keys and prefixes with a given prefix and which do not contain the character
293299
“/” after the given prefix.
@@ -298,8 +304,11 @@ def list_dir(self, prefix: str) -> AsyncGenerator[str]:
298304
299305
Returns
300306
-------
301-
AsyncGenerator[str, None]
307+
AsyncIterator[str]
302308
"""
309+
# This method should be async, like overridden methods in child classes.
310+
# However, that's not straightforward:
311+
# https://stackoverflow.com/questions/68905848
303312

304313
async def delete_dir(self, prefix: str) -> None:
305314
"""

src/zarr/core/array.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,29 @@ def from_dict(
645645
store_path: StorePath,
646646
data: dict[str, JSON],
647647
) -> AsyncArray[ArrayV3Metadata] | AsyncArray[ArrayV2Metadata]:
648+
"""
649+
Create a Zarr array from a dictionary, with support for both Zarr v2 and v3 metadata.
650+
651+
Parameters
652+
----------
653+
store_path : StorePath
654+
The path within the store where the array should be created.
655+
656+
data : dict
657+
A dictionary representing the array data. This dictionary should include necessary metadata
658+
for the array, such as shape, dtype, and other attributes. The format of the metadata
659+
will determine whether a Zarr v2 or v3 array is created.
660+
661+
Returns
662+
-------
663+
AsyncArray[ArrayV3Metadata] or AsyncArray[ArrayV2Metadata]
664+
The created Zarr array, either using v2 or v3 metadata based on the provided data.
665+
666+
Raises
667+
------
668+
ValueError
669+
If the dictionary data is invalid or incompatible with either Zarr v2 or v3 array creation.
670+
"""
648671
metadata = parse_array_metadata(data)
649672
return cls(metadata=metadata, store_path=store_path)
650673

@@ -1040,6 +1063,9 @@ async def getitem(
10401063
return await self._get_selection(indexer, prototype=prototype)
10411064

10421065
async def _save_metadata(self, metadata: ArrayMetadata, ensure_parents: bool = False) -> None:
1066+
"""
1067+
Asynchronously save the array metadata.
1068+
"""
10431069
to_save = metadata.to_buffer_dict(default_buffer_prototype())
10441070
awaitables = [set_or_delete(self.store_path / key, value) for key, value in to_save.items()]
10451071

@@ -1118,6 +1144,39 @@ async def setitem(
11181144
value: npt.ArrayLike,
11191145
prototype: BufferPrototype | None = None,
11201146
) -> None:
1147+
"""
1148+
Asynchronously set values in the array using basic indexing.
1149+
1150+
Parameters
1151+
----------
1152+
selection : BasicSelection
1153+
The selection defining the region of the array to set.
1154+
1155+
value : numpy.typing.ArrayLike
1156+
The values to be written into the selected region of the array.
1157+
1158+
prototype : BufferPrototype or None, optional
1159+
A prototype buffer that defines the structure and properties of the array chunks being modified.
1160+
If None, the default buffer prototype is used. Default is None.
1161+
1162+
Returns
1163+
-------
1164+
None
1165+
This method does not return any value.
1166+
1167+
Raises
1168+
------
1169+
IndexError
1170+
If the selection is out of bounds for the array.
1171+
1172+
ValueError
1173+
If the values are not compatible with the array's dtype or shape.
1174+
1175+
Notes
1176+
-----
1177+
- This method is asynchronous and should be awaited.
1178+
- Supports basic indexing, where the selection is contiguous and does not involve advanced indexing.
1179+
"""
11211180
if prototype is None:
11221181
prototype = default_buffer_prototype()
11231182
indexer = BasicIndexer(
@@ -1128,6 +1187,32 @@ async def setitem(
11281187
return await self._set_selection(indexer, value, prototype=prototype)
11291188

11301189
async def resize(self, new_shape: ShapeLike, delete_outside_chunks: bool = True) -> None:
1190+
"""
1191+
Asynchronously resize the array to a new shape.
1192+
1193+
Parameters
1194+
----------
1195+
new_shape : ChunkCoords
1196+
The desired new shape of the array.
1197+
1198+
delete_outside_chunks : bool, optional
1199+
If True (default), chunks that fall outside the new shape will be deleted. If False,
1200+
the data in those chunks will be preserved.
1201+
1202+
Returns
1203+
-------
1204+
AsyncArray
1205+
The resized array.
1206+
1207+
Raises
1208+
------
1209+
ValueError
1210+
If the new shape is incompatible with the current array's chunking configuration.
1211+
1212+
Notes
1213+
-----
1214+
- This method is asynchronous and should be awaited.
1215+
"""
11311216
new_shape = parse_shapelike(new_shape)
11321217
assert len(new_shape) == len(self.metadata.shape)
11331218
new_metadata = self.metadata.update_shape(new_shape)
@@ -1210,6 +1295,31 @@ async def append(self, data: npt.ArrayLike, axis: int = 0) -> ChunkCoords:
12101295
return new_shape
12111296

12121297
async def update_attributes(self, new_attributes: dict[str, JSON]) -> Self:
1298+
"""
1299+
Asynchronously update the array's attributes.
1300+
1301+
Parameters
1302+
----------
1303+
new_attributes : dict of str to JSON
1304+
A dictionary of new attributes to update or add to the array. The keys represent attribute
1305+
names, and the values must be JSON-compatible.
1306+
1307+
Returns
1308+
-------
1309+
AsyncArray
1310+
The array with the updated attributes.
1311+
1312+
Raises
1313+
------
1314+
ValueError
1315+
If the attributes are invalid or incompatible with the array's metadata.
1316+
1317+
Notes
1318+
-----
1319+
- This method is asynchronous and should be awaited.
1320+
- The updated attributes will be merged with existing attributes, and any conflicts will be
1321+
overwritten by the new values.
1322+
"""
12131323
# metadata.attributes is "frozen" so we simply clear and update the dict
12141324
self.metadata.attributes.clear()
12151325
self.metadata.attributes.update(new_attributes)
@@ -1328,6 +1438,28 @@ def from_dict(
13281438
store_path: StorePath,
13291439
data: dict[str, JSON],
13301440
) -> Array:
1441+
"""
1442+
Create a Zarr array from a dictionary.
1443+
1444+
Parameters
1445+
----------
1446+
store_path : StorePath
1447+
The path within the store where the array should be created.
1448+
1449+
data : dict
1450+
A dictionary representing the array data. This dictionary should include necessary metadata
1451+
for the array, such as shape, dtype, fill value, and attributes.
1452+
1453+
Returns
1454+
-------
1455+
Array
1456+
The created Zarr array.
1457+
1458+
Raises
1459+
------
1460+
ValueError
1461+
If the dictionary data is invalid or missing required fields for array creation.
1462+
"""
13311463
async_array = AsyncArray.from_dict(store_path=store_path, data=data)
13321464
return cls(async_array)
13331465

@@ -2934,6 +3066,30 @@ def append(self, data: npt.ArrayLike, axis: int = 0) -> ChunkCoords:
29343066
return sync(self._async_array.append(data, axis=axis))
29353067

29363068
def update_attributes(self, new_attributes: dict[str, JSON]) -> Array:
3069+
"""
3070+
Update the array's attributes.
3071+
3072+
Parameters
3073+
----------
3074+
new_attributes : dict
3075+
A dictionary of new attributes to update or add to the array. The keys represent attribute
3076+
names, and the values must be JSON-compatible.
3077+
3078+
Returns
3079+
-------
3080+
Array
3081+
The array with the updated attributes.
3082+
3083+
Raises
3084+
------
3085+
ValueError
3086+
If the attributes are invalid or incompatible with the array's metadata.
3087+
3088+
Notes
3089+
-----
3090+
- The updated attributes will be merged with existing attributes, and any conflicts will be
3091+
overwritten by the new values.
3092+
"""
29373093
# TODO: remove this cast when type inference improves
29383094
new_array = sync(self._async_array.update_attributes(new_attributes))
29393095
# TODO: remove this cast when type inference improves

src/zarr/storage/local.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from zarr.core.common import concurrent_map
1212

1313
if TYPE_CHECKING:
14-
from collections.abc import AsyncGenerator, Iterable
14+
from collections.abc import AsyncIterator, Iterable
1515

1616
from zarr.core.buffer import BufferPrototype
1717

@@ -198,22 +198,22 @@ async def exists(self, key: str) -> bool:
198198
path = self.root / key
199199
return await asyncio.to_thread(path.is_file)
200200

201-
async def list(self) -> AsyncGenerator[str]:
201+
async def list(self) -> AsyncIterator[str]:
202202
# docstring inherited
203203
to_strip = self.root.as_posix() + "/"
204204
for p in list(self.root.rglob("*")):
205205
if p.is_file():
206206
yield p.as_posix().replace(to_strip, "")
207207

208-
async def list_prefix(self, prefix: str) -> AsyncGenerator[str]:
208+
async def list_prefix(self, prefix: str) -> AsyncIterator[str]:
209209
# docstring inherited
210210
to_strip = self.root.as_posix() + "/"
211211
prefix = prefix.rstrip("/")
212212
for p in (self.root / prefix).rglob("*"):
213213
if p.is_file():
214214
yield p.as_posix().replace(to_strip, "")
215215

216-
async def list_dir(self, prefix: str) -> AsyncGenerator[str]:
216+
async def list_dir(self, prefix: str) -> AsyncIterator[str]:
217217
# docstring inherited
218218
base = self.root / prefix
219219
try:

src/zarr/storage/logging.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from zarr.abc.store import ByteRangeRequest, Store
1111

1212
if TYPE_CHECKING:
13-
from collections.abc import AsyncGenerator, Generator, Iterable
13+
from collections.abc import AsyncIterator, Generator, Iterable
1414

1515
from zarr.core.buffer import Buffer, BufferPrototype
1616

@@ -203,19 +203,19 @@ async def set_partial_values(
203203
with self.log(keys):
204204
return await self._store.set_partial_values(key_start_values=key_start_values)
205205

206-
async def list(self) -> AsyncGenerator[str]:
206+
async def list(self) -> AsyncIterator[str]:
207207
# docstring inherited
208208
with self.log():
209209
async for key in self._store.list():
210210
yield key
211211

212-
async def list_prefix(self, prefix: str) -> AsyncGenerator[str]:
212+
async def list_prefix(self, prefix: str) -> AsyncIterator[str]:
213213
# docstring inherited
214214
with self.log(prefix):
215215
async for key in self._store.list_prefix(prefix=prefix):
216216
yield key
217217

218-
async def list_dir(self, prefix: str) -> AsyncGenerator[str]:
218+
async def list_dir(self, prefix: str) -> AsyncIterator[str]:
219219
# docstring inherited
220220
with self.log(prefix):
221221
async for key in self._store.list_dir(prefix=prefix):

src/zarr/storage/memory.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from zarr.storage._utils import _normalize_interval_index
1010

1111
if TYPE_CHECKING:
12-
from collections.abc import AsyncGenerator, Iterable, MutableMapping
12+
from collections.abc import AsyncIterator, Iterable, MutableMapping
1313

1414
from zarr.core.buffer import BufferPrototype
1515

@@ -138,19 +138,19 @@ async def set_partial_values(self, key_start_values: Iterable[tuple[str, int, by
138138
# docstring inherited
139139
raise NotImplementedError
140140

141-
async def list(self) -> AsyncGenerator[str]:
141+
async def list(self) -> AsyncIterator[str]:
142142
# docstring inherited
143143
for key in self._store_dict:
144144
yield key
145145

146-
async def list_prefix(self, prefix: str) -> AsyncGenerator[str]:
146+
async def list_prefix(self, prefix: str) -> AsyncIterator[str]:
147147
# docstring inherited
148148
# note: we materialize all dict keys into a list here so we can mutate the dict in-place (e.g. in delete_prefix)
149149
for key in list(self._store_dict):
150150
if key.startswith(prefix):
151151
yield key
152152

153-
async def list_dir(self, prefix: str) -> AsyncGenerator[str]:
153+
async def list_dir(self, prefix: str) -> AsyncIterator[str]:
154154
# docstring inherited
155155
prefix = prefix.rstrip("/")
156156

src/zarr/storage/remote.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from zarr.storage.common import _dereference_path
88

99
if TYPE_CHECKING:
10-
from collections.abc import AsyncGenerator, Iterable
10+
from collections.abc import AsyncIterator, Iterable
1111

1212
from fsspec.asyn import AsyncFileSystem
1313

@@ -303,13 +303,13 @@ async def set_partial_values(
303303
# docstring inherited
304304
raise NotImplementedError
305305

306-
async def list(self) -> AsyncGenerator[str]:
306+
async def list(self) -> AsyncIterator[str]:
307307
# docstring inherited
308308
allfiles = await self.fs._find(self.path, detail=False, withdirs=False)
309309
for onefile in (a.replace(self.path + "/", "") for a in allfiles):
310310
yield onefile
311311

312-
async def list_dir(self, prefix: str) -> AsyncGenerator[str]:
312+
async def list_dir(self, prefix: str) -> AsyncIterator[str]:
313313
# docstring inherited
314314
prefix = f"{self.path}/{prefix.rstrip('/')}"
315315
try:
@@ -319,7 +319,7 @@ async def list_dir(self, prefix: str) -> AsyncGenerator[str]:
319319
for onefile in (a.replace(prefix + "/", "") for a in allfiles):
320320
yield onefile.removeprefix(self.path).removeprefix("/")
321321

322-
async def list_prefix(self, prefix: str) -> AsyncGenerator[str]:
322+
async def list_prefix(self, prefix: str) -> AsyncIterator[str]:
323323
# docstring inherited
324324
for onefile in await self.fs._find(
325325
f"{self.path}/{prefix}", detail=False, maxdepth=None, withdirs=False

0 commit comments

Comments
 (0)