Skip to content

Commit 3423f39

Browse files
committed
Merge branch 'main' of https://github.com/zarr-developers/zarr-python into feat/serializable-protocol
2 parents f110af5 + 8ca385a commit 3423f39

13 files changed

Lines changed: 207 additions & 30 deletions

File tree

.github/workflows/needs_release_notes.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
name: "Pull Request Labeler"
22

33
on:
4-
- pull_request_target:
5-
types: [opened, reopened, synchronize]
4+
pull_request_target:
5+
types: [opened, reopened, synchronize]
66

77
jobs:
88
labeler:

changes/3797.bugfix.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix an issue that prevents the correct parsing of special NumPy ``uint32`` dtypes resulting e.g.
2+
from bit wise operations on ``uint32`` arrays on Windows.

changes/3828.misc.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
`CodecPipeline.read` and `CodecPipeline.read_batch` now return a tuple of typeddict objects
2+
that each carry information about the request for a chunk from storage.

changes/3836.doc.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Corrects the type annotation reported for the `batch_info` parameter in the `CodecPipeline.write`
2+
method docstring.

mkdocs.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,6 @@ plugins:
216216
'developers/index.html.md': 'contributing.md'
217217
'developers/roadmap.html.md': 'https://zarr.readthedocs.io/en/v3.0.8/developers/roadmap.html'
218218
'api/zarr/creation.md': 'api/zarr/deprecated/creation.md'
219-
'api/zarr/codecs/numcodecs.md': 'api/zarr/deprecated/creation.md'
220219
'api.md': 'api/zarr/index.md'
221220
'api/zarr/metadata/migrate_v3.md': 'api/zarr/metadata.md'
222221

src/zarr/abc/codec.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from abc import abstractmethod
44
from collections.abc import Mapping
5-
from typing import TYPE_CHECKING, Generic, Protocol, TypeGuard, TypeVar, runtime_checkable
5+
from typing import TYPE_CHECKING, Generic, Literal, Protocol, TypeGuard, TypeVar, runtime_checkable
66

77
from typing_extensions import ReadOnly, TypedDict
88

@@ -32,9 +32,17 @@
3232
"CodecInput",
3333
"CodecOutput",
3434
"CodecPipeline",
35+
"GetResult",
3536
"SupportsSyncCodec",
3637
]
3738

39+
40+
class GetResult(TypedDict):
41+
"""Metadata about a store get operation."""
42+
43+
status: Literal["present", "missing"]
44+
45+
3846
CodecInput = TypeVar("CodecInput", bound=NDBuffer | Buffer)
3947
CodecOutput = TypeVar("CodecOutput", bound=NDBuffer | Buffer)
4048

@@ -433,13 +441,13 @@ async def read(
433441
batch_info: Iterable[tuple[ByteGetter, ArraySpec, SelectorTuple, SelectorTuple, bool]],
434442
out: NDBuffer,
435443
drop_axes: tuple[int, ...] = (),
436-
) -> None:
444+
) -> tuple[GetResult, ...]:
437445
"""Reads chunk data from the store, decodes it and writes it into an output array.
438446
Partial decoding may be utilized if the codecs and stores support it.
439447
440448
Parameters
441449
----------
442-
batch_info : Iterable[tuple[ByteGetter, ArraySpec, SelectorTuple, SelectorTuple]]
450+
batch_info : Iterable[tuple[ByteGetter, ArraySpec, SelectorTuple, SelectorTuple, bool]]
443451
Ordered set of information about the chunks.
444452
The first slice selection determines which parts of the chunk will be fetched.
445453
The second slice selection determines where in the output array the chunk data will be written.
@@ -451,6 +459,11 @@ async def read(
451459
``out``) to the fill value for the array.
452460
453461
out : NDBuffer
462+
463+
Returns
464+
-------
465+
tuple[GetResult, ...]
466+
One result per chunk in ``batch_info``.
454467
"""
455468
...
456469

@@ -467,7 +480,7 @@ async def write(
467480
468481
Parameters
469482
----------
470-
batch_info : Iterable[tuple[ByteSetter, ArraySpec, SelectorTuple, SelectorTuple]]
483+
batch_info : Iterable[tuple[ByteSetter, ArraySpec, SelectorTuple, SelectorTuple, bool]]
471484
Ordered set of information about the chunks.
472485
The first slice selection determines which parts of the chunk will be encoded.
473486
The second slice selection determines where in the value array the chunk data is located.

src/zarr/codecs/sharding.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from collections.abc import Iterable, Mapping, MutableMapping
3+
from collections.abc import Iterable, Mapping, MutableMapping, Sequence
44
from dataclasses import dataclass, replace
55
from enum import Enum
66
from functools import lru_cache
@@ -45,6 +45,7 @@
4545
from zarr.core.dtype.npy.int import UInt64
4646
from zarr.core.indexing import (
4747
BasicIndexer,
48+
ChunkProjection,
4849
SelectorTuple,
4950
_morton_order,
5051
_morton_order_keys,
@@ -574,21 +575,26 @@ async def _encode_partial_single(
574575
chunks_per_shard = self._get_chunks_per_shard(shard_spec)
575576
chunk_spec = self._get_chunk_spec(shard_spec)
576577

577-
shard_reader = await self._load_full_shard_maybe(
578-
byte_getter=byte_setter,
579-
prototype=chunk_spec.prototype,
580-
chunks_per_shard=chunks_per_shard,
581-
)
582-
shard_reader = shard_reader or _ShardReader.create_empty(chunks_per_shard)
583-
# Use vectorized lookup for better performance
584-
shard_dict = shard_reader.to_dict_vectorized(np.asarray(_morton_order(chunks_per_shard)))
585-
586578
indexer = list(
587579
get_indexer(
588580
selection, shape=shard_shape, chunk_grid=RegularChunkGrid(chunk_shape=chunk_shape)
589581
)
590582
)
591583

584+
if self._is_complete_shard_write(indexer, chunks_per_shard):
585+
shard_dict = dict.fromkeys(morton_order_iter(chunks_per_shard))
586+
else:
587+
shard_reader = await self._load_full_shard_maybe(
588+
byte_getter=byte_setter,
589+
prototype=chunk_spec.prototype,
590+
chunks_per_shard=chunks_per_shard,
591+
)
592+
shard_reader = shard_reader or _ShardReader.create_empty(chunks_per_shard)
593+
# Use vectorized lookup for better performance
594+
shard_dict = shard_reader.to_dict_vectorized(
595+
np.asarray(_morton_order(chunks_per_shard))
596+
)
597+
592598
await self.codec_pipeline.write(
593599
[
594600
(
@@ -661,6 +667,16 @@ def _is_total_shard(
661667
chunk_coords in all_chunk_coords for chunk_coords in c_order_iter(chunks_per_shard)
662668
)
663669

670+
def _is_complete_shard_write(
671+
self,
672+
indexed_chunks: Sequence[ChunkProjection],
673+
chunks_per_shard: tuple[int, ...],
674+
) -> bool:
675+
all_chunk_coords = {chunk_coords for chunk_coords, *_ in indexed_chunks}
676+
return self._is_total_shard(all_chunk_coords, chunks_per_shard) and all(
677+
is_complete_chunk for *_, is_complete_chunk in indexed_chunks
678+
)
679+
664680
async def _decode_shard_index(
665681
self, index_bytes: Buffer, chunks_per_shard: tuple[int, ...]
666682
) -> _ShardIndex:

src/zarr/core/codec_pipeline.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
BytesBytesCodec,
1414
Codec,
1515
CodecPipeline,
16+
GetResult,
1617
)
1718
from zarr.core.common import concurrent_map
1819
from zarr.core.config import config
@@ -251,47 +252,58 @@ async def read_batch(
251252
batch_info: Iterable[tuple[ByteGetter, ArraySpec, SelectorTuple, SelectorTuple, bool]],
252253
out: NDBuffer,
253254
drop_axes: tuple[int, ...] = (),
254-
) -> None:
255+
) -> tuple[GetResult, ...]:
256+
results: list[GetResult] = []
255257
if self.supports_partial_decode:
258+
batch_info_list = list(batch_info)
256259
chunk_array_batch = await self.decode_partial_batch(
257260
[
258261
(byte_getter, chunk_selection, chunk_spec)
259-
for byte_getter, chunk_spec, chunk_selection, *_ in batch_info
262+
for byte_getter, chunk_spec, chunk_selection, *_ in batch_info_list
260263
]
261264
)
262265
for chunk_array, (_, chunk_spec, _, out_selection, _) in zip(
263-
chunk_array_batch, batch_info, strict=False
266+
chunk_array_batch, batch_info_list, strict=False
264267
):
265268
if chunk_array is not None:
266269
if drop_axes:
267270
chunk_array = chunk_array.squeeze(axis=drop_axes)
268271
out[out_selection] = chunk_array
272+
results.append(GetResult(status="present"))
269273
else:
270274
out[out_selection] = fill_value_or_default(chunk_spec)
275+
results.append(GetResult(status="missing"))
271276
else:
277+
batch_info_list = list(batch_info)
272278
chunk_bytes_batch = await concurrent_map(
273-
[(byte_getter, array_spec.prototype) for byte_getter, array_spec, *_ in batch_info],
279+
[
280+
(byte_getter, array_spec.prototype)
281+
for byte_getter, array_spec, *_ in batch_info_list
282+
],
274283
lambda byte_getter, prototype: byte_getter.get(prototype),
275284
config.get("async.concurrency"),
276285
)
277286
chunk_array_batch = await self.decode_batch(
278287
[
279288
(chunk_bytes, chunk_spec)
280289
for chunk_bytes, (_, chunk_spec, *_) in zip(
281-
chunk_bytes_batch, batch_info, strict=False
290+
chunk_bytes_batch, batch_info_list, strict=False
282291
)
283292
],
284293
)
285294
for chunk_array, (_, chunk_spec, chunk_selection, out_selection, _) in zip(
286-
chunk_array_batch, batch_info, strict=False
295+
chunk_array_batch, batch_info_list, strict=False
287296
):
288297
if chunk_array is not None:
289298
tmp = chunk_array[chunk_selection]
290299
if drop_axes:
291300
tmp = tmp.squeeze(axis=drop_axes)
292301
out[out_selection] = tmp
302+
results.append(GetResult(status="present"))
293303
else:
294304
out[out_selection] = fill_value_or_default(chunk_spec)
305+
results.append(GetResult(status="missing"))
306+
return tuple(results)
295307

296308
def _merge_chunk_array(
297309
self,
@@ -471,15 +483,19 @@ async def read(
471483
batch_info: Iterable[tuple[ByteGetter, ArraySpec, SelectorTuple, SelectorTuple, bool]],
472484
out: NDBuffer,
473485
drop_axes: tuple[int, ...] = (),
474-
) -> None:
475-
await concurrent_map(
486+
) -> tuple[GetResult, ...]:
487+
batch_results = await concurrent_map(
476488
[
477489
(single_batch_info, out, drop_axes)
478490
for single_batch_info in batched(batch_info, self.batch_size)
479491
],
480492
self.read_batch,
481493
config.get("async.concurrency"),
482494
)
495+
results: list[GetResult] = []
496+
for batch in batch_results:
497+
results.extend(batch)
498+
return tuple(results)
483499

484500
async def write(
485501
self,

src/zarr/core/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
AccessModeLiteral = Literal["r", "r+", "a", "w", "w-"]
5050
ANY_ACCESS_MODE: Final = "r", "r+", "a", "w", "w-"
5151
DimensionNamesLike = Iterable[str | None] | None
52-
DimensionNames = DimensionNamesLike # backwards compatibility
52+
DimensionNames = DimensionNamesLike # for backwards compatibility
5353

5454
TName = TypeVar("TName", bound=str)
5555
TConfig = TypeVar("TConfig", bound=Mapping[str, object])

src/zarr/core/dtype/npy/int.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,6 +1070,28 @@ class UInt32(BaseInt[np.dtypes.UInt32DType, np.uint32], HasEndianness):
10701070
_zarr_v3_name: ClassVar[Literal["uint32"]] = "uint32"
10711071
_zarr_v2_names: ClassVar[tuple[Literal[">u4"], Literal["<u4"]]] = (">u4", "<u4")
10721072

1073+
@classmethod
1074+
def _check_native_dtype(cls: type[Self], dtype: TBaseDType) -> TypeGuard[np.dtypes.UInt32DType]:
1075+
"""
1076+
A type guard that checks if the input is assignable to the type of ``cls.dtype_class``
1077+
1078+
This method is overridden for this particular data type because of a Windows-specific issue
1079+
where ``np.array([1], dtype=np.uint32) & 1`` creates an instance of ``np.dtypes.UIntDType``,
1080+
rather than an instance of ``np.dtypes.UInt32DType``, even though both represent 32-bit
1081+
unsigned integers. (In contrast to ``np.dtype('i')``, ``np.dtype('u')`` raises an error.)
1082+
1083+
Parameters
1084+
----------
1085+
dtype : TDType
1086+
The dtype to check.
1087+
1088+
Returns
1089+
-------
1090+
Bool
1091+
True if the dtype matches, False otherwise.
1092+
"""
1093+
return super()._check_native_dtype(dtype) or dtype == np.dtypes.UInt32DType()
1094+
10731095
@classmethod
10741096
def from_native_dtype(cls, dtype: TBaseDType) -> Self:
10751097
"""

0 commit comments

Comments
 (0)