Skip to content

Commit 6900754

Browse files
Basic Zarr-python 2.x compatibility changes (#2098)
* WIP - backwards compat * fixup put * rm consolidated * typing fixup * revert unneded change * fixup * deprecate positional args * attribute * Fixup * fixup * fixup * fixup * fixup * fixup * fixup * fixup * fixup * fixup * fixup * fixup * fixup * ci * fixup * fixup --------- Co-authored-by: Joe Hamman <[email protected]>
1 parent c878da2 commit 6900754

15 files changed

+303
-61
lines changed

Diff for: pyproject.toml

+2-1
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 = [

Diff for: src/zarr/_compat.py

+68
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]

Diff for: src/zarr/api/asynchronous.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ async def group(
503503
try:
504504
return await AsyncGroup.open(store=store_path, zarr_format=zarr_format)
505505
except (KeyError, FileNotFoundError):
506-
return await AsyncGroup.create(
506+
return await AsyncGroup.from_store(
507507
store=store_path,
508508
zarr_format=zarr_format or _default_zarr_version(),
509509
exists_ok=overwrite,
@@ -512,8 +512,8 @@ async def group(
512512

513513

514514
async def open_group(
515-
*, # Note: this is a change from v2
516515
store: StoreLike | None = None,
516+
*, # Note: this is a change from v2
517517
mode: AccessModeLiteral | None = None,
518518
cache_attrs: bool | None = None, # not used, default changed
519519
synchronizer: Any = None, # not used
@@ -590,7 +590,7 @@ async def open_group(
590590
try:
591591
return await AsyncGroup.open(store_path, zarr_format=zarr_format)
592592
except (KeyError, FileNotFoundError):
593-
return await AsyncGroup.create(
593+
return await AsyncGroup.from_store(
594594
store_path,
595595
zarr_format=zarr_format or _default_zarr_version(),
596596
exists_ok=True,

Diff for: src/zarr/api/synchronous.py

+8-3
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,
@@ -159,9 +162,10 @@ def array(data: NDArrayLike, **kwargs: Any) -> Array:
159162
return Array(sync(async_api.array(data=data, **kwargs)))
160163

161164

165+
@_deprecate_positional_args
162166
def group(
163-
*, # Note: this is a change from v2
164167
store: StoreLike | None = None,
168+
*, # Note: this is a change from v2
165169
overwrite: bool = False,
166170
chunk_store: StoreLike | None = None, # not used in async_api
167171
cache_attrs: bool | None = None, # default changed, not used in async_api
@@ -190,9 +194,10 @@ def group(
190194
)
191195

192196

197+
@_deprecate_positional_args
193198
def open_group(
194-
*, # Note: this is a change from v2
195199
store: StoreLike | None = None,
200+
*, # Note: this is a change from v2
196201
mode: AccessModeLiteral | None = None, # not used in async api
197202
cache_attrs: bool | None = None, # default changed, not used in async api
198203
synchronizer: Any = None, # not used in async api

Diff for: src/zarr/core/array.py

+13
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import numpy as np
99
import numpy.typing as npt
1010

11+
from zarr._compat import _deprecate_positional_args
12+
from zarr.abc.codec import Codec, CodecPipeline
1113
from zarr.abc.store import set_or_delete
1214
from zarr.codecs import BytesCodec
1315
from zarr.codecs._v2 import V2Compressor, V2Filters
@@ -621,6 +623,7 @@ class Array:
621623
_async_array: AsyncArray
622624

623625
@classmethod
626+
@_deprecate_positional_args
624627
def create(
625628
cls,
626629
store: StoreLike,
@@ -1016,6 +1019,7 @@ def __setitem__(self, selection: Selection, value: npt.ArrayLike) -> None:
10161019
else:
10171020
self.set_basic_selection(cast(BasicSelection, pure_selection), value, fields=fields)
10181021

1022+
@_deprecate_positional_args
10191023
def get_basic_selection(
10201024
self,
10211025
selection: BasicSelection = Ellipsis,
@@ -1139,6 +1143,7 @@ def get_basic_selection(
11391143
)
11401144
)
11411145

1146+
@_deprecate_positional_args
11421147
def set_basic_selection(
11431148
self,
11441149
selection: BasicSelection,
@@ -1234,6 +1239,7 @@ def set_basic_selection(
12341239
indexer = BasicIndexer(selection, self.shape, self.metadata.chunk_grid)
12351240
sync(self._async_array._set_selection(indexer, value, fields=fields, prototype=prototype))
12361241

1242+
@_deprecate_positional_args
12371243
def get_orthogonal_selection(
12381244
self,
12391245
selection: OrthogonalSelection,
@@ -1358,6 +1364,7 @@ def get_orthogonal_selection(
13581364
)
13591365
)
13601366

1367+
@_deprecate_positional_args
13611368
def set_orthogonal_selection(
13621369
self,
13631370
selection: OrthogonalSelection,
@@ -1468,6 +1475,7 @@ def set_orthogonal_selection(
14681475
self._async_array._set_selection(indexer, value, fields=fields, prototype=prototype)
14691476
)
14701477

1478+
@_deprecate_positional_args
14711479
def get_mask_selection(
14721480
self,
14731481
mask: MaskSelection,
@@ -1550,6 +1558,7 @@ def get_mask_selection(
15501558
)
15511559
)
15521560

1561+
@_deprecate_positional_args
15531562
def set_mask_selection(
15541563
self,
15551564
mask: MaskSelection,
@@ -1628,6 +1637,7 @@ def set_mask_selection(
16281637
indexer = MaskIndexer(mask, self.shape, self.metadata.chunk_grid)
16291638
sync(self._async_array._set_selection(indexer, value, fields=fields, prototype=prototype))
16301639

1640+
@_deprecate_positional_args
16311641
def get_coordinate_selection(
16321642
self,
16331643
selection: CoordinateSelection,
@@ -1717,6 +1727,7 @@ def get_coordinate_selection(
17171727
out_array = np.array(out_array).reshape(indexer.sel_shape)
17181728
return out_array
17191729

1730+
@_deprecate_positional_args
17201731
def set_coordinate_selection(
17211732
self,
17221733
selection: CoordinateSelection,
@@ -1806,6 +1817,7 @@ def set_coordinate_selection(
18061817

18071818
sync(self._async_array._set_selection(indexer, value, fields=fields, prototype=prototype))
18081819

1820+
@_deprecate_positional_args
18091821
def get_block_selection(
18101822
self,
18111823
selection: BasicSelection,
@@ -1904,6 +1916,7 @@ def get_block_selection(
19041916
)
19051917
)
19061918

1919+
@_deprecate_positional_args
19071920
def set_block_selection(
19081921
self,
19091922
selection: BasicSelection,

Diff for: src/zarr/core/attributes.py

+16
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,19 @@ def __iter__(self) -> Iterator[str]:
3535

3636
def __len__(self) -> int:
3737
return len(self._obj.metadata.attributes)
38+
39+
def put(self, d: dict[str, JSON]) -> None:
40+
"""
41+
Overwrite all attributes with the values from `d`.
42+
43+
Equivalent to the following pseudo-code, but performed atomically.
44+
45+
.. code-block:: python
46+
47+
>>> attrs = {"a": 1, "b": 2}
48+
>>> attrs.clear()
49+
>>> attrs.update({"a": 3", "c": 4})
50+
>>> attrs
51+
{'a': 3, 'c': 4}
52+
"""
53+
self._obj = self._obj.update_attributes(d)

Diff for: src/zarr/core/group.py

+40-5
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import zarr.api.asynchronous as async_api
1414
from zarr.abc.metadata import Metadata
15-
from zarr.abc.store import set_or_delete
15+
from zarr.abc.store import Store, set_or_delete
1616
from zarr.core.array import Array, AsyncArray
1717
from zarr.core.attributes import Attributes
1818
from zarr.core.buffer import default_buffer_prototype
@@ -126,7 +126,7 @@ class AsyncGroup:
126126
store_path: StorePath
127127

128128
@classmethod
129-
async def create(
129+
async def from_store(
130130
cls,
131131
store: StoreLike,
132132
*,
@@ -312,6 +312,21 @@ def attrs(self) -> dict[str, Any]:
312312
def info(self) -> None:
313313
raise NotImplementedError
314314

315+
@property
316+
def store(self) -> Store:
317+
return self.store_path.store
318+
319+
@property
320+
def read_only(self) -> bool:
321+
# Backwards compatibility for 2.x
322+
return self.store_path.store.mode.readonly
323+
324+
@property
325+
def synchronizer(self) -> None:
326+
# Backwards compatibility for 2.x
327+
# Not implemented in 3.x yet.
328+
return None
329+
315330
async def create_group(
316331
self,
317332
name: str,
@@ -320,7 +335,7 @@ async def create_group(
320335
attributes: dict[str, Any] | None = None,
321336
) -> AsyncGroup:
322337
attributes = attributes or {}
323-
return await type(self).create(
338+
return await type(self).from_store(
324339
self.store_path / name,
325340
attributes=attributes,
326341
exists_ok=exists_ok,
@@ -752,7 +767,7 @@ class Group(SyncMixin):
752767
_async_group: AsyncGroup
753768

754769
@classmethod
755-
def create(
770+
def from_store(
756771
cls,
757772
store: StoreLike,
758773
*,
@@ -762,7 +777,7 @@ def create(
762777
) -> Group:
763778
attributes = attributes or {}
764779
obj = sync(
765-
AsyncGroup.create(
780+
AsyncGroup.from_store(
766781
store,
767782
attributes=attributes,
768783
exists_ok=exists_ok,
@@ -843,6 +858,22 @@ def attrs(self) -> Attributes:
843858
def info(self) -> None:
844859
raise NotImplementedError
845860

861+
@property
862+
def store(self) -> Store:
863+
# Backwards compatibility for 2.x
864+
return self._async_group.store
865+
866+
@property
867+
def read_only(self) -> bool:
868+
# Backwards compatibility for 2.x
869+
return self._async_group.read_only
870+
871+
@property
872+
def synchronizer(self) -> None:
873+
# Backwards compatibility for 2.x
874+
# Not implemented in 3.x yet.
875+
return self._async_group.synchronizer
876+
846877
def update_attributes(self, new_attributes: dict[str, Any]) -> Group:
847878
self._sync(self._async_group.update_attributes(new_attributes))
848879
return self
@@ -913,6 +944,10 @@ def require_groups(self, *names: str) -> tuple[Group, ...]:
913944
"""Convenience method to require multiple groups in a single call."""
914945
return tuple(map(Group, self._sync(self._async_group.require_groups(*names))))
915946

947+
def create(self, *args: Any, **kwargs: Any) -> Array:
948+
# Backwards compatibility for 2.x
949+
return self.create_array(*args, **kwargs)
950+
916951
def create_array(
917952
self,
918953
name: str,

Diff for: src/zarr/testing/strategies.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def arrays(
9999
expected_attrs = {} if attributes is None else attributes
100100

101101
array_path = path + ("/" if not path.endswith("/") else "") + name
102-
root = Group.create(store)
102+
root = Group.from_store(store)
103103
fill_value_args: tuple[Any, ...] = tuple()
104104
if nparray.dtype.kind == "M":
105105
m = re.search(r"\[(.+)\]", nparray.dtype.str)

Diff for: tests/v3/conftest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ async def async_group(request: pytest.FixtureRequest, tmpdir: LEGACY_PATH) -> As
8989
param: AsyncGroupRequest = request.param
9090

9191
store = await parse_store(param.store, str(tmpdir))
92-
agroup = await AsyncGroup.create(
92+
agroup = await AsyncGroup.from_store(
9393
store,
9494
attributes=param.attributes,
9595
zarr_format=param.zarr_format,

0 commit comments

Comments
 (0)