|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | 3 | import json |
| 4 | +import warnings |
4 | 5 | from collections.abc import Iterable, Mapping, Sequence |
5 | 6 | from dataclasses import dataclass, field, replace |
6 | 7 | from typing import TYPE_CHECKING, Any, Final, Literal, NotRequired, TypeGuard, cast |
7 | 8 |
|
| 9 | +import numpy as np |
8 | 10 | from typing_extensions import TypedDict |
9 | 11 |
|
10 | 12 | from zarr.abc.codec import ArrayArrayCodec, ArrayBytesCodec, BytesBytesCodec, Codec |
|
35 | 37 | from zarr.core.dtype import VariableLengthUTF8, ZDType, get_data_type_from_json |
36 | 38 | from zarr.core.dtype.common import check_dtype_spec_v3 |
37 | 39 | from zarr.core.json_parse import parse_field |
38 | | -from zarr.core.metadata.common import parse_attributes |
39 | | -from zarr.errors import MetadataValidationError, NodeTypeValidationError |
| 40 | +from zarr.core.metadata.common import RESAVE_METADATA_HINT, parse_attributes |
| 41 | +from zarr.errors import MetadataValidationError, NodeTypeValidationError, ZarrUserWarning |
40 | 42 | from zarr.registry import get_codec_class |
41 | 43 |
|
42 | 44 | if TYPE_CHECKING: |
@@ -476,6 +478,51 @@ class ArrayMetadataJSON_V3(TypedDict, extra_items=AllowedExtraField): # type: i |
476 | 478 | } |
477 | 479 |
|
478 | 480 |
|
| 481 | +def _read_legacy_zero_chunk_sizes( |
| 482 | + chunk_grid: dict[str, JSON] | ChunkGridMetadata | NamedConfig[str, Any], |
| 483 | + shape: tuple[int, ...], |
| 484 | +) -> dict[str, JSON] | ChunkGridMetadata | NamedConfig[str, Any]: |
| 485 | + """Read a stored regular chunk size of 0 on a zero-length axis as 1. |
| 486 | +
|
| 487 | + zarr-python 3.0 and 3.1 stored `chunk_shape: [0]` for an array created with |
| 488 | + a zero-length axis and `chunks=(0,)` or `chunks=False` (3.0 stored `false` |
| 489 | + for the latter). Chunk sizes must be at least 1, and a zero-length axis has |
| 490 | + no chunks for the size to describe until the array grows, so the size is |
| 491 | + read as 1 with a warning. This is the policy `ArrayV2Metadata` applies to |
| 492 | + Zarr format 2 metadata. It needs the array shape, which the chunk grid |
| 493 | + metadata does not carry, so it runs here rather than in the grid parser. |
| 494 | + A zero chunk size on a positive-length axis is left for that parser to |
| 495 | + reject. |
| 496 | + """ |
| 497 | + if not isinstance(chunk_grid, Mapping) or chunk_grid.get("name") != "regular": |
| 498 | + return chunk_grid |
| 499 | + configuration = chunk_grid.get("configuration") |
| 500 | + if not isinstance(configuration, Mapping): |
| 501 | + return chunk_grid |
| 502 | + chunk_shape = configuration.get("chunk_shape") |
| 503 | + if not isinstance(chunk_shape, Sequence) or isinstance(chunk_shape, str): |
| 504 | + return chunk_grid |
| 505 | + if len(chunk_shape) != len(shape): |
| 506 | + return chunk_grid # the dimensionality check reports this |
| 507 | + normalized = list(chunk_shape) |
| 508 | + changed = False |
| 509 | + for dim_idx, (size, extent) in enumerate(zip(chunk_shape, shape, strict=True)): |
| 510 | + if isinstance(size, int | np.integer) and size == 0 and extent == 0: |
| 511 | + warnings.warn( |
| 512 | + f"Dimension {dim_idx}: chunk edge length {size!r} on a zero-length axis " |
| 513 | + "(as written by zarr-python 3.0 and 3.1) is treated as 1. " |
| 514 | + f"{RESAVE_METADATA_HINT}", |
| 515 | + ZarrUserWarning, |
| 516 | + stacklevel=2, |
| 517 | + ) |
| 518 | + normalized[dim_idx] = 1 |
| 519 | + changed = True |
| 520 | + if not changed: |
| 521 | + return chunk_grid |
| 522 | + corrected: dict[str, JSON] = {**configuration, "chunk_shape": normalized} |
| 523 | + return {"name": "regular", "configuration": corrected} |
| 524 | + |
| 525 | + |
479 | 526 | @dataclass(frozen=True, kw_only=True) |
480 | 527 | class ArrayV3Metadata(Metadata): |
481 | 528 | shape: tuple[int, ...] |
@@ -510,7 +557,9 @@ def __init__( |
510 | 557 | """ |
511 | 558 |
|
512 | 559 | shape_parsed = parse_shapelike(shape) |
513 | | - chunk_grid_parsed = parse_chunk_grid(chunk_grid) |
| 560 | + chunk_grid_parsed = parse_chunk_grid( |
| 561 | + _read_legacy_zero_chunk_sizes(chunk_grid, shape_parsed) |
| 562 | + ) |
514 | 563 | chunk_key_encoding_parsed = parse_chunk_key_encoding(chunk_key_encoding) |
515 | 564 | dimension_names_parsed = parse_dimension_names(dimension_names) |
516 | 565 | # Note: relying on a type method is numpy-specific |
|
0 commit comments