Skip to content

Commit 7800f38

Browse files
committed
Merge branch 'v3' of https://github.com/zarr-developers/zarr-python into fix/dask-compat
2 parents dea4a3d + 6900754 commit 7800f38

29 files changed

+1021
-435
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ test = [
6666
"flask",
6767
"requests",
6868
"mypy",
69-
"hypothesis"
69+
"hypothesis",
70+
"universal-pathlib",
7071
]
7172

7273
jupyter = [

src/zarr/_compat.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import warnings
2+
from collections.abc import Callable
3+
from functools import wraps
4+
from inspect import Parameter, signature
5+
from typing import Any, TypeVar
6+
7+
T = TypeVar("T")
8+
9+
# Based off https://github.com/scikit-learn/scikit-learn/blob/e87b32a81c70abed8f2e97483758eb64df8255e9/sklearn/utils/validation.py#L63
10+
11+
12+
def _deprecate_positional_args(
13+
func: Callable[..., T] | None = None, *, version: str = "3.1.0"
14+
) -> Callable[..., T]:
15+
"""Decorator for methods that issues warnings for positional arguments.
16+
17+
Using the keyword-only argument syntax in pep 3102, arguments after the
18+
* will issue a warning when passed as a positional argument.
19+
20+
Parameters
21+
----------
22+
func : callable, default=None
23+
Function to check arguments on.
24+
version : callable, default="3.1.0"
25+
The version when positional arguments will result in error.
26+
"""
27+
28+
def _inner_deprecate_positional_args(f: Callable[..., T]) -> Callable[..., T]:
29+
sig = signature(f)
30+
kwonly_args = []
31+
all_args = []
32+
33+
for name, param in sig.parameters.items():
34+
if param.kind == Parameter.POSITIONAL_OR_KEYWORD:
35+
all_args.append(name)
36+
elif param.kind == Parameter.KEYWORD_ONLY:
37+
kwonly_args.append(name)
38+
39+
@wraps(f)
40+
def inner_f(*args: Any, **kwargs: Any) -> T:
41+
extra_args = len(args) - len(all_args)
42+
if extra_args <= 0:
43+
return f(*args, **kwargs)
44+
45+
# extra_args > 0
46+
args_msg = [
47+
f"{name}={arg}"
48+
for name, arg in zip(kwonly_args[:extra_args], args[-extra_args:], strict=False)
49+
]
50+
formatted_args_msg = ", ".join(args_msg)
51+
warnings.warn(
52+
(
53+
f"Pass {formatted_args_msg} as keyword args. From version "
54+
f"{version} passing these as positional arguments "
55+
"will result in an error"
56+
),
57+
FutureWarning,
58+
stacklevel=2,
59+
)
60+
kwargs.update(zip(sig.parameters, args, strict=False))
61+
return f(**kwargs)
62+
63+
return inner_f
64+
65+
if func is not None:
66+
return _inner_deprecate_positional_args(func)
67+
68+
return _inner_deprecate_positional_args # type: ignore[return-value]

src/zarr/abc/store.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from abc import ABC, abstractmethod
2-
from collections.abc import AsyncGenerator
2+
from asyncio import gather
3+
from collections.abc import AsyncGenerator, Iterable
34
from typing import Any, NamedTuple, Protocol, runtime_checkable
45

56
from typing_extensions import Self
@@ -158,6 +159,13 @@ async def set(self, key: str, value: Buffer) -> None:
158159
"""
159160
...
160161

162+
async def _set_many(self, values: Iterable[tuple[str, Buffer]]) -> None:
163+
"""
164+
Insert multiple (key, value) pairs into storage.
165+
"""
166+
await gather(*(self.set(key, value) for key, value in values))
167+
return None
168+
161169
@property
162170
@abstractmethod
163171
def supports_deletes(self) -> bool:
@@ -211,7 +219,9 @@ def list(self) -> AsyncGenerator[str, None]:
211219

212220
@abstractmethod
213221
def list_prefix(self, prefix: str) -> AsyncGenerator[str, None]:
214-
"""Retrieve all keys in the store with a given prefix.
222+
"""
223+
Retrieve all keys in the store that begin with a given prefix. Keys are returned with the
224+
common leading prefix removed.
215225
216226
Parameters
217227
----------

src/zarr/api/asynchronous.py

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ async def open(
196196
zarr_version: ZarrFormat | None = None, # deprecated
197197
zarr_format: ZarrFormat | None = None,
198198
path: str | None = None,
199+
storage_options: dict[str, Any] | None = None,
199200
**kwargs: Any, # TODO: type kwargs as valid args to open_array
200201
) -> AsyncArray | AsyncGroup:
201202
"""Convenience function to open a group or array using file-mode-like semantics.
@@ -213,6 +214,9 @@ async def open(
213214
The zarr format to use when saving.
214215
path : str or None, optional
215216
The path within the store to open.
217+
storage_options : dict
218+
If using an fsspec URL to create the store, these will be passed to
219+
the backend implementation. Ignored otherwise.
216220
**kwargs
217221
Additional parameters are passed through to :func:`zarr.creation.open_array` or
218222
:func:`zarr.hierarchy.open_group`.
@@ -279,6 +283,7 @@ async def save_array(
279283
zarr_version: ZarrFormat | None = None, # deprecated
280284
zarr_format: ZarrFormat | None = None,
281285
path: str | None = None,
286+
storage_options: dict[str, Any] | None = None,
282287
**kwargs: Any, # TODO: type kwargs as valid args to create
283288
) -> None:
284289
"""Convenience function to save a NumPy array to the local file system, following a
@@ -294,6 +299,9 @@ async def save_array(
294299
The zarr format to use when saving.
295300
path : str or None, optional
296301
The path within the store where the array will be saved.
302+
storage_options : dict
303+
If using an fsspec URL to create the store, these will be passed to
304+
the backend implementation. Ignored otherwise.
297305
kwargs
298306
Passed through to :func:`create`, e.g., compressor.
299307
"""
@@ -329,6 +337,7 @@ async def save_group(
329337
zarr_version: ZarrFormat | None = None, # deprecated
330338
zarr_format: ZarrFormat | None = None,
331339
path: str | None = None,
340+
storage_options: dict[str, Any] | None = None,
332341
**kwargs: NDArrayLike,
333342
) -> None:
334343
"""Convenience function to save several NumPy arrays to the local file system, following a
@@ -344,22 +353,40 @@ async def save_group(
344353
The zarr format to use when saving.
345354
path : str or None, optional
346355
Path within the store where the group will be saved.
356+
storage_options : dict
357+
If using an fsspec URL to create the store, these will be passed to
358+
the backend implementation. Ignored otherwise.
347359
kwargs
348360
NumPy arrays with data to save.
349361
"""
350362
zarr_format = (
351-
_handle_zarr_version_or_format(zarr_version=zarr_version, zarr_format=zarr_format)
363+
_handle_zarr_version_or_format(
364+
zarr_version=zarr_version,
365+
zarr_format=zarr_format,
366+
)
352367
or _default_zarr_version()
353368
)
354369

355370
if len(args) == 0 and len(kwargs) == 0:
356371
raise ValueError("at least one array must be provided")
357372
aws = []
358373
for i, arr in enumerate(args):
359-
aws.append(save_array(store, arr, zarr_format=zarr_format, path=f"{path}/arr_{i}"))
374+
aws.append(
375+
save_array(
376+
store,
377+
arr,
378+
zarr_format=zarr_format,
379+
path=f"{path}/arr_{i}",
380+
storage_options=storage_options,
381+
)
382+
)
360383
for k, arr in kwargs.items():
361384
_path = f"{path}/{k}" if path is not None else k
362-
aws.append(save_array(store, arr, zarr_format=zarr_format, path=_path))
385+
aws.append(
386+
save_array(
387+
store, arr, zarr_format=zarr_format, path=_path, storage_options=storage_options
388+
)
389+
)
363390
await asyncio.gather(*aws)
364391

365392

@@ -428,6 +455,7 @@ async def group(
428455
zarr_format: ZarrFormat | None = None,
429456
meta_array: Any | None = None, # not used
430457
attributes: dict[str, JSON] | None = None,
458+
storage_options: dict[str, Any] | None = None,
431459
) -> AsyncGroup:
432460
"""Create a group.
433461
@@ -454,6 +482,9 @@ async def group(
454482
to users. Use `numpy.empty(())` by default.
455483
zarr_format : {2, 3, None}, optional
456484
The zarr format to use when saving.
485+
storage_options : dict
486+
If using an fsspec URL to create the store, these will be passed to
487+
the backend implementation. Ignored otherwise.
457488
458489
Returns
459490
-------
@@ -484,7 +515,7 @@ async def group(
484515
try:
485516
return await AsyncGroup.open(store=store_path, zarr_format=zarr_format)
486517
except (KeyError, FileNotFoundError):
487-
return await AsyncGroup.create(
518+
return await AsyncGroup.from_store(
488519
store=store_path,
489520
zarr_format=zarr_format or _default_zarr_version(),
490521
exists_ok=overwrite,
@@ -493,14 +524,14 @@ async def group(
493524

494525

495526
async def open_group(
496-
*, # Note: this is a change from v2
497527
store: StoreLike | None = None,
528+
*, # Note: this is a change from v2
498529
mode: AccessModeLiteral | None = None,
499530
cache_attrs: bool | None = None, # not used, default changed
500531
synchronizer: Any = None, # not used
501532
path: str | None = None,
502533
chunk_store: StoreLike | None = None, # not used
503-
storage_options: dict[str, Any] | None = None, # not used
534+
storage_options: dict[str, Any] | None = None,
504535
zarr_version: ZarrFormat | None = None, # deprecated
505536
zarr_format: ZarrFormat | None = None,
506537
meta_array: Any | None = None, # not used
@@ -560,11 +591,6 @@ async def open_group(
560591
warnings.warn("meta_array is not yet implemented", RuntimeWarning, stacklevel=2)
561592
if chunk_store is not None:
562593
warnings.warn("chunk_store is not yet implemented", RuntimeWarning, stacklevel=2)
563-
if storage_options is not None:
564-
warnings.warn("storage_options is not yet implemented", RuntimeWarning, stacklevel=2)
565-
566-
if mode is not None and isinstance(store, Store | StorePath):
567-
raise ValueError("mode cannot be set when store is already initialized")
568594

569595
store_path = await make_store_path(store, mode=mode)
570596
if path is not None:
@@ -576,7 +602,7 @@ async def open_group(
576602
try:
577603
return await AsyncGroup.open(store_path, zarr_format=zarr_format)
578604
except (KeyError, FileNotFoundError):
579-
return await AsyncGroup.create(
605+
return await AsyncGroup.from_store(
580606
store_path,
581607
zarr_format=zarr_format or _default_zarr_version(),
582608
exists_ok=True,
@@ -590,7 +616,7 @@ async def create(
590616
chunks: ChunkCoords | None = None, # TODO: v2 allowed chunks=True
591617
dtype: npt.DTypeLike | None = None,
592618
compressor: dict[str, JSON] | None = None, # TODO: default and type change
593-
fill_value: Any = 0, # TODO: need type
619+
fill_value: Any | None = 0, # TODO: need type
594620
order: MemoryOrder | None = None, # TODO: default change
595621
store: str | StoreLike | None = None,
596622
synchronizer: Any | None = None,
@@ -618,6 +644,7 @@ async def create(
618644
) = None,
619645
codecs: Iterable[Codec | dict[str, JSON]] | None = None,
620646
dimension_names: Iterable[str] | None = None,
647+
storage_options: dict[str, Any] | None = None,
621648
**kwargs: Any,
622649
) -> AsyncArray:
623650
"""Create an array.
@@ -689,6 +716,9 @@ async def create(
689716
to users. Use `numpy.empty(())` by default.
690717
691718
.. versionadded:: 2.13
719+
storage_options : dict
720+
If using an fsspec URL to create the store, these will be passed to
721+
the backend implementation. Ignored otherwise.
692722
693723
Returns
694724
-------
@@ -848,7 +878,7 @@ async def full_like(a: ArrayLike, **kwargs: Any) -> AsyncArray:
848878
"""
849879
like_kwargs = _like_args(a, kwargs)
850880
if isinstance(a, AsyncArray):
851-
kwargs.setdefault("fill_value", a.metadata.fill_value)
881+
like_kwargs.setdefault("fill_value", a.metadata.fill_value)
852882
return await full(**like_kwargs)
853883

854884

@@ -896,6 +926,7 @@ async def open_array(
896926
zarr_version: ZarrFormat | None = None, # deprecated
897927
zarr_format: ZarrFormat | None = None,
898928
path: PathLike | None = None,
929+
storage_options: dict[str, Any] | None = None,
899930
**kwargs: Any, # TODO: type kwargs as valid args to save
900931
) -> AsyncArray:
901932
"""Open an array using file-mode-like semantics.
@@ -908,6 +939,9 @@ async def open_array(
908939
The zarr format to use when saving.
909940
path : string, optional
910941
Path in store to array.
942+
storage_options : dict
943+
If using an fsspec URL to create the store, these will be passed to
944+
the backend implementation. Ignored otherwise.
911945
**kwargs
912946
Any keyword arguments to pass to the array constructor.
913947

src/zarr/api/synchronous.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import TYPE_CHECKING, Any
44

55
import zarr.api.asynchronous as async_api
6+
from zarr._compat import _deprecate_positional_args
67
from zarr.core.array import Array, AsyncArray
78
from zarr.core.group import Group
89
from zarr.core.sync import sync
@@ -63,9 +64,10 @@ def load(
6364
return sync(async_api.load(store=store, zarr_version=zarr_version, path=path))
6465

6566

67+
@_deprecate_positional_args
6668
def open(
67-
*,
6869
store: StoreLike | None = None,
70+
*,
6971
mode: AccessModeLiteral | None = None, # type and value changed
7072
zarr_version: ZarrFormat | None = None, # deprecated
7173
zarr_format: ZarrFormat | None = None,
@@ -107,6 +109,7 @@ def save(
107109
)
108110

109111

112+
@_deprecate_positional_args
110113
def save_array(
111114
store: StoreLike,
112115
arr: NDArrayLike,
@@ -134,6 +137,7 @@ def save_group(
134137
zarr_version: ZarrFormat | None = None, # deprecated
135138
zarr_format: ZarrFormat | None = None,
136139
path: str | None = None,
140+
storage_options: dict[str, Any] | None = None,
137141
**kwargs: NDArrayLike,
138142
) -> None:
139143
return sync(
@@ -143,6 +147,7 @@ def save_group(
143147
zarr_version=zarr_version,
144148
zarr_format=zarr_format,
145149
path=path,
150+
storage_options=storage_options,
146151
**kwargs,
147152
)
148153
)
@@ -157,9 +162,10 @@ def array(data: NDArrayLike, **kwargs: Any) -> Array:
157162
return Array(sync(async_api.array(data=data, **kwargs)))
158163

159164

165+
@_deprecate_positional_args
160166
def group(
161-
*, # Note: this is a change from v2
162167
store: StoreLike | None = None,
168+
*, # Note: this is a change from v2
163169
overwrite: bool = False,
164170
chunk_store: StoreLike | None = None, # not used in async_api
165171
cache_attrs: bool | None = None, # default changed, not used in async_api
@@ -188,9 +194,10 @@ def group(
188194
)
189195

190196

197+
@_deprecate_positional_args
191198
def open_group(
192-
*, # Note: this is a change from v2
193199
store: StoreLike | None = None,
200+
*, # Note: this is a change from v2
194201
mode: AccessModeLiteral | None = None, # not used in async api
195202
cache_attrs: bool | None = None, # default changed, not used in async api
196203
synchronizer: Any = None, # not used in async api

0 commit comments

Comments
 (0)