Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic Zarr-python 2.x compatibility changes #2098

Merged
merged 31 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0c8e7f7
WIP - backwards compat
TomAugspurger Aug 18, 2024
fa9e51d
fixup put
TomAugspurger Aug 19, 2024
8fdb605
rm consolidated
TomAugspurger Aug 19, 2024
0ac17cc
typing fixup
TomAugspurger Aug 19, 2024
be2b3cd
revert unneded change
TomAugspurger Aug 19, 2024
94933b3
fixup
TomAugspurger Aug 19, 2024
44ad6c7
deprecate positional args
TomAugspurger Aug 28, 2024
08e7f3c
attribute
TomAugspurger Aug 28, 2024
f937468
Fixup
TomAugspurger Aug 29, 2024
784cb28
Merge remote-tracking branch 'upstream/v3' into user/tom/fix/v2-compat
TomAugspurger Sep 3, 2024
b01e61c
Merge remote-tracking branch 'upstream/v3' into user/tom/fix/v2-compat
TomAugspurger Sep 6, 2024
c519fbe
fixup
TomAugspurger Sep 6, 2024
46c2c11
fixup
TomAugspurger Sep 6, 2024
1933f57
fixup
TomAugspurger Sep 6, 2024
36a2ceb
fixup
TomAugspurger Sep 6, 2024
1ae1cfd
fixup
TomAugspurger Sep 6, 2024
9f5429f
fixup
TomAugspurger Sep 6, 2024
a5ad0ca
fixup
TomAugspurger Sep 6, 2024
3d04845
Merge remote-tracking branch 'upstream/v3' into user/tom/fix/v2-compat
TomAugspurger Sep 16, 2024
b710c64
fixup
TomAugspurger Sep 16, 2024
0ab29d1
fixup
TomAugspurger Sep 16, 2024
aa1e3bc
fixup
TomAugspurger Sep 16, 2024
fb4bb85
fixup
TomAugspurger Sep 16, 2024
1cc84ab
fixup
TomAugspurger Sep 16, 2024
559399e
ci
TomAugspurger Sep 16, 2024
16bad38
Merge remote-tracking branch 'upstream/v3' into user/tom/fix/v2-compat
TomAugspurger Sep 19, 2024
f7f8457
Merge branch 'v3' into user/tom/fix/v2-compat
jhamman Sep 19, 2024
56431fe
Merge remote-tracking branch 'upstream/v3' into user/tom/fix/v2-compat
TomAugspurger Sep 19, 2024
b13dee3
fixup
TomAugspurger Sep 19, 2024
348aed8
Merge remote-tracking branch 'upstream/v3' into user/tom/fix/v2-compat
TomAugspurger Sep 20, 2024
2327141
fixup
TomAugspurger Sep 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ test = [
"flask",
"requests",
"mypy",
"hypothesis"
"hypothesis",
"universal-pathlib",
]

jupyter = [
Expand Down
68 changes: 68 additions & 0 deletions src/zarr/_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import warnings
from collections.abc import Callable
from functools import wraps
from inspect import Parameter, signature
from typing import Any, TypeVar

T = TypeVar("T")

# Based off https://github.com/scikit-learn/scikit-learn/blob/e87b32a81c70abed8f2e97483758eb64df8255e9/sklearn/utils/validation.py#L63


def _deprecate_positional_args(
func: Callable[..., T] | None = None, *, version: str = "1.3"
) -> Callable[..., T]:
"""Decorator for methods that issues warnings for positional arguments.

Using the keyword-only argument syntax in pep 3102, arguments after the
* will issue a warning when passed as a positional argument.

Parameters
----------
func : callable, default=None
Function to check arguments on.
version : callable, default="1.3"
The version when positional arguments will result in error.
"""

def _inner_deprecate_positional_args(f: Callable[..., T]) -> Callable[..., T]:
sig = signature(f)
kwonly_args = []
all_args = []

for name, param in sig.parameters.items():
if param.kind == Parameter.POSITIONAL_OR_KEYWORD:
all_args.append(name)
elif param.kind == Parameter.KEYWORD_ONLY:
kwonly_args.append(name)

@wraps(f)
def inner_f(*args: Any, **kwargs: Any) -> T:
extra_args = len(args) - len(all_args)
if extra_args <= 0:
return f(*args, **kwargs)

# extra_args > 0
args_msg = [
f"{name}={arg}"
for name, arg in zip(kwonly_args[:extra_args], args[-extra_args:], strict=False)
]
formatted_args_msg = ", ".join(args_msg)
warnings.warn(
(
f"Pass {formatted_args_msg} as keyword args. From version "
f"{version} passing these as positional arguments "
"will result in an error"
),
FutureWarning,
stacklevel=2,
)
kwargs.update(zip(sig.parameters, args, strict=False))
return f(**kwargs)

return inner_f

if func is not None:
return _inner_deprecate_positional_args(func)

return _inner_deprecate_positional_args # type: ignore[return-value]
6 changes: 3 additions & 3 deletions src/zarr/api/asynchronous.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ async def group(
try:
return await AsyncGroup.open(store=store_path, zarr_format=zarr_format)
except (KeyError, FileNotFoundError):
return await AsyncGroup.create(
return await AsyncGroup.from_store(
store=store_path,
zarr_format=zarr_format,
exists_ok=overwrite,
Expand All @@ -477,8 +477,8 @@ async def group(


async def open_group(
*, # Note: this is a change from v2
store: StoreLike | None = None,
*, # Note: this is a change from v2
mode: AccessModeLiteral | None = None, # not used
cache_attrs: bool | None = None, # not used, default changed
synchronizer: Any = None, # not used
Expand Down Expand Up @@ -550,7 +550,7 @@ async def open_group(
try:
return await AsyncGroup.open(store_path, zarr_format=zarr_format)
except (KeyError, FileNotFoundError):
return await AsyncGroup.create(
return await AsyncGroup.from_store(
store_path, zarr_format=zarr_format, exists_ok=True, attributes=attributes
)

Expand Down
11 changes: 8 additions & 3 deletions src/zarr/api/synchronous.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Any

import zarr.api.asynchronous as async_api
from zarr._compat import _deprecate_positional_args
from zarr.core.array import Array, AsyncArray
from zarr.core.buffer import NDArrayLike
from zarr.core.common import JSON, AccessModeLiteral, ChunkCoords, ZarrFormat
Expand Down Expand Up @@ -61,9 +62,10 @@ def load(
return sync(async_api.load(store=store, zarr_version=zarr_version, path=path))


@_deprecate_positional_args
def open(
*,
store: StoreLike | None = None,
*,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can vendor and use https://github.com/scikit-learn/scikit-learn/blob/e87b32a81c70abed8f2e97483758eb64df8255e9/sklearn/utils/validation.py#L32 to deprecate positional arguments. At least of xarray, it passed just store as a positional argument, but deprecating these doesn't feel like too much work.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this suggestion. Should we do it in 2.18.3 or 3.0 though?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a strong preference here.

IMO, calendar time is more meaningful than version numbers for giving projects time to adapt to changes. So I'd say deprecate it in 3.0, as long as we don't have a hard stance on requiring keyword only for these APIs in 3.0

mode: AccessModeLiteral | None = None, # type and value changed
zarr_version: ZarrFormat | None = None, # deprecated
zarr_format: ZarrFormat | None = None,
Expand Down Expand Up @@ -105,6 +107,7 @@ def save(
)


@_deprecate_positional_args
def save_array(
store: StoreLike,
arr: NDArrayLike,
Expand Down Expand Up @@ -155,9 +158,10 @@ def array(data: NDArrayLike, **kwargs: Any) -> Array:
return Array(sync(async_api.array(data=data, **kwargs)))


@_deprecate_positional_args
def group(
*, # Note: this is a change from v2
store: StoreLike | None = None,
*, # Note: this is a change from v2
overwrite: bool = False,
chunk_store: StoreLike | None = None, # not used in async_api
cache_attrs: bool | None = None, # default changed, not used in async_api
Expand Down Expand Up @@ -186,9 +190,10 @@ def group(
)


@_deprecate_positional_args
def open_group(
*, # Note: this is a change from v2
store: StoreLike | None = None,
*, # Note: this is a change from v2
mode: AccessModeLiteral | None = None, # not used in async api
cache_attrs: bool | None = None, # default changed, not used in async api
synchronizer: Any = None, # not used in async api
Expand Down
12 changes: 12 additions & 0 deletions src/zarr/core/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import numpy as np
import numpy.typing as npt

from zarr._compat import _deprecate_positional_args
from zarr.abc.codec import Codec, CodecPipeline
from zarr.abc.store import set_or_delete
from zarr.codecs import BytesCodec
Expand Down Expand Up @@ -613,6 +614,7 @@ class Array:
_async_array: AsyncArray

@classmethod
@_deprecate_positional_args
def create(
cls,
store: StoreLike,
Expand Down Expand Up @@ -1008,6 +1010,7 @@ def __setitem__(self, selection: Selection, value: npt.ArrayLike) -> None:
else:
self.set_basic_selection(cast(BasicSelection, pure_selection), value, fields=fields)

@_deprecate_positional_args
def get_basic_selection(
self,
selection: BasicSelection = Ellipsis,
Expand Down Expand Up @@ -1131,6 +1134,7 @@ def get_basic_selection(
)
)

@_deprecate_positional_args
def set_basic_selection(
self,
selection: BasicSelection,
Expand Down Expand Up @@ -1226,6 +1230,7 @@ def set_basic_selection(
indexer = BasicIndexer(selection, self.shape, self.metadata.chunk_grid)
sync(self._async_array._set_selection(indexer, value, fields=fields, prototype=prototype))

@_deprecate_positional_args
def get_orthogonal_selection(
self,
selection: OrthogonalSelection,
Expand Down Expand Up @@ -1350,6 +1355,7 @@ def get_orthogonal_selection(
)
)

@_deprecate_positional_args
def set_orthogonal_selection(
self,
selection: OrthogonalSelection,
Expand Down Expand Up @@ -1460,6 +1466,7 @@ def set_orthogonal_selection(
self._async_array._set_selection(indexer, value, fields=fields, prototype=prototype)
)

@_deprecate_positional_args
def get_mask_selection(
self,
mask: MaskSelection,
Expand Down Expand Up @@ -1542,6 +1549,7 @@ def get_mask_selection(
)
)

@_deprecate_positional_args
def set_mask_selection(
self,
mask: MaskSelection,
Expand Down Expand Up @@ -1620,6 +1628,7 @@ def set_mask_selection(
indexer = MaskIndexer(mask, self.shape, self.metadata.chunk_grid)
sync(self._async_array._set_selection(indexer, value, fields=fields, prototype=prototype))

@_deprecate_positional_args
def get_coordinate_selection(
self,
selection: CoordinateSelection,
Expand Down Expand Up @@ -1709,6 +1718,7 @@ def get_coordinate_selection(
out_array = np.array(out_array).reshape(indexer.sel_shape)
return out_array

@_deprecate_positional_args
def set_coordinate_selection(
self,
selection: CoordinateSelection,
Expand Down Expand Up @@ -1798,6 +1808,7 @@ def set_coordinate_selection(

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

@_deprecate_positional_args
def get_block_selection(
self,
selection: BasicSelection,
Expand Down Expand Up @@ -1896,6 +1907,7 @@ def get_block_selection(
)
)

@_deprecate_positional_args
def set_block_selection(
self,
selection: BasicSelection,
Expand Down
16 changes: 16 additions & 0 deletions src/zarr/core/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,19 @@ def __iter__(self) -> Iterator[str]:

def __len__(self) -> int:
return len(self._obj.metadata.attributes)

def put(self, d: dict[str, JSON]) -> None:
"""
Overwrite all attributes with the values from `d`.
Equivalent to the following pseudo-code, but performed atomically.
.. code-block:: python
>>> attrs = {"a": 1, "b": 2}
>>> attrs.clear()
>>> attrs.update({"a": 3", "c": 4})
>>> attrs
{'a': 3, 'c': 4}
"""
self._obj = self._obj.update_attributes(d)
45 changes: 40 additions & 5 deletions src/zarr/core/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from zarr.abc.codec import Codec
from zarr.abc.metadata import Metadata
from zarr.abc.store import set_or_delete
from zarr.abc.store import Store, set_or_delete
from zarr.core.array import Array, AsyncArray
from zarr.core.attributes import Attributes
from zarr.core.buffer import default_buffer_prototype
Expand Down Expand Up @@ -123,7 +123,7 @@ class AsyncGroup:
store_path: StorePath

@classmethod
async def create(
async def from_store(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 2.x, Group.create was used to create an array in the Group.

I slightly prefer from_* for alternative constructors anyway, so maybe this is a strict improvement and not just a compatibility shim?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that this needs to change in order to avoid confusion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm also a fan of from_X, and from_store seems good

cls,
store: StoreLike,
*,
Expand Down Expand Up @@ -309,6 +309,21 @@ def attrs(self) -> dict[str, Any]:
def info(self) -> None:
raise NotImplementedError

@property
def store(self) -> Store:
return self.store_path.store

@property
def read_only(self) -> bool:
# Backwards compatibility for 2.x
return self.store_path.store.mode.readonly

@property
def synchronizer(self) -> None:
# Backwards compatibility for 2.x
# Not implemented in 3.x yet.
return None

async def create_group(
self,
name: str,
Expand All @@ -317,7 +332,7 @@ async def create_group(
attributes: dict[str, Any] | None = None,
) -> AsyncGroup:
attributes = attributes or {}
return await type(self).create(
return await type(self).from_store(
self.store_path / name,
attributes=attributes,
exists_ok=exists_ok,
Expand Down Expand Up @@ -588,7 +603,7 @@ class Group(SyncMixin):
_async_group: AsyncGroup

@classmethod
def create(
def from_store(
cls,
store: StoreLike,
*,
Expand All @@ -598,7 +613,7 @@ def create(
) -> Group:
attributes = attributes or {}
obj = sync(
AsyncGroup.create(
AsyncGroup.from_store(
store,
attributes=attributes,
exists_ok=exists_ok,
Expand Down Expand Up @@ -678,6 +693,22 @@ def attrs(self) -> Attributes:
def info(self) -> None:
raise NotImplementedError

@property
def store(self) -> Store:
# Backwards compatibility for 2.x
return self._async_group.store

@property
def read_only(self) -> bool:
# Backwards compatibility for 2.x
return self._async_group.read_only

@property
def synchronizer(self) -> None:
# Backwards compatibility for 2.x
# Not implemented in 3.x yet.
return self._async_group.synchronizer

def update_attributes(self, new_attributes: dict[str, Any]) -> Group:
self._sync(self._async_group.update_attributes(new_attributes))
return self
Expand Down Expand Up @@ -717,6 +748,10 @@ def tree(self, expand: bool = False, level: int | None = None) -> Any:
def create_group(self, name: str, **kwargs: Any) -> Group:
return Group(self._sync(self._async_group.create_group(name, **kwargs)))

def create(self, *args: Any, **kwargs: Any) -> Array:
# Backwards compatibility for 2.x
return self.create_array(*args, **kwargs)

def create_array(
self,
name: str,
Expand Down
3 changes: 3 additions & 0 deletions src/zarr/core/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,9 @@ def parse_fill_value_v2(fill_value: Any, dtype: np.dtype[Any]) -> Any:
# todo: r* dtypes


# type[bool] | type[signedinteger[_8Bit]] | type[signedinteger[_16Bit]] | type[signedinteger[_32Bit]] | type[signedinteger[_64Bit]] | type[unsignedinteger[_8Bit]] | type[unsignedinteger[_16Bit]] | type[unsignedinteger[_32Bit]] | type[unsignedinteger[_64Bit]] | type[floating[_16Bit]] | type[floating[_32Bit]] | type[floating[_64Bit]] | type[complexfloating[_32Bit, _32Bit]] | type[complexfloating[_64Bit, _64Bit]]


@overload
def parse_fill_value_v3(fill_value: Any, dtype: BOOL_DTYPE) -> BOOL: ...

Expand Down
2 changes: 1 addition & 1 deletion src/zarr/testing/strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def arrays(
expected_attrs = {} if attributes is None else attributes

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