Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions changes/4492.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Raise a `ValueError` for out-of-bounds integer fill values instead of leaking NumPy's `OverflowError`, both when reading stored metadata and when casting values at array creation.
39 changes: 31 additions & 8 deletions src/zarr/core/dtype/npy/int.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,26 @@ def _cast_scalar_unchecked(self, data: IntLike) -> Scalar:

return self.to_native_dtype().type(data) # type: ignore[return-value]

def _check_int_bounds(self, data: object, value: int) -> None:
"""
Check that an integer value fits in this data type's range.

Parameters
----------
data : object
The original input, used in the error message.
value : int
The integer value to bounds-check.

Raises
------
ValueError
If ``value`` is outside the range of the native dtype.
"""
info = np.iinfo(self.to_native_dtype())
if value < info.min or value > info.max:
raise ValueError(f"{data!r} is out of bounds for {self.to_native_dtype()}.")

def cast_scalar(self, data: object) -> Scalar:
"""
Attempt to cast a given object to a NumPy integer scalar.
Expand All @@ -196,9 +216,12 @@ def cast_scalar(self, data: object) -> Scalar:
------
TypeError
If the data cannot be converted to a NumPy integer scalar.
ValueError
If the data is an integer outside the range of the dtype.
"""

if self._check_scalar(data):
self._check_int_bounds(data, int(data))
return self._cast_scalar_unchecked(data)
msg = (
f"Cannot convert object {data!r} with type {type(data)} to a scalar compatible with the "
Expand Down Expand Up @@ -237,16 +260,16 @@ def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> Scalar:
------
TypeError
If the input is not a valid integer type.
ValueError
If the input is an integer outside the range of the dtype.
"""
if check_json_int(data):
return self._cast_scalar_unchecked(data)
if check_json_intish_float(data):
return self._cast_scalar_unchecked(int(data))

if check_json_intish_str(data):
return self._cast_scalar_unchecked(int(data))
if check_json_int(data) or check_json_intish_float(data) or check_json_intish_str(data):
value = int(data)
else:
raise TypeError(f"Invalid type: {data}. Expected an integer.")

raise TypeError(f"Invalid type: {data}. Expected an integer.")
self._check_int_bounds(data, value)
return self._cast_scalar_unchecked(value)

def to_json_scalar(self, data: object, *, zarr_format: ZarrFormat) -> int:
"""
Expand Down
58 changes: 57 additions & 1 deletion tests/test_dtype/test_npy/test_int.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import Any
from typing import TYPE_CHECKING, Any

import numpy as np
import pytest
Expand All @@ -10,6 +10,9 @@
from zarr.core.dtype.npy.int import Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64
from zarr.errors import DataTypeValidationError

if TYPE_CHECKING:
from zarr.core.common import JSON


class TestInt8(BaseTestZDType):
test_cls = Int8
Expand Down Expand Up @@ -365,3 +368,56 @@ def test_match_dtype_not_a_numpy_dtype(not_a_dtype: Any) -> None:
"""
with pytest.raises(ValueError, match="No Zarr data type found"):
data_type_registry.match_dtype(not_a_dtype)


@pytest.mark.parametrize(
("dtype_instance", "value"),
[
(Int8(), 300),
(Int8(), -129),
(Int8(), "300"),
(Int8(), 300.0),
(UInt8(), -1),
(UInt64(), 2**64),
(Int64(), -(2**63) - 1),
],
)
def test_out_of_bounds_integer_from_json_scalar(
dtype_instance: Int8 | UInt8 | UInt64 | Int64, value: JSON
) -> None:
"""An integer outside the dtype's range raises a validation error, not
numpy's OverflowError. Regression test for
https://github.com/zarr-developers/zarr-python/issues/4453 item 4."""
for zarr_format in (2, 3):
with pytest.raises(ValueError, match="out of bounds"):
dtype_instance.from_json_scalar(value, zarr_format=zarr_format)


@pytest.mark.parametrize(
("dtype_instance", "value"),
[
(Int8(), 127),
(Int8(), -128),
(UInt8(), 255),
(UInt64(), 2**64 - 1),
(Int64(), -(2**63)),
],
)
def test_in_bounds_integer_from_json_scalar(
dtype_instance: Int8 | UInt8 | UInt64 | Int64, value: int
) -> None:
"""In-bounds integers round-trip for both zarr formats."""
for zarr_format in (2, 3):
assert dtype_instance.from_json_scalar(
value, zarr_format=zarr_format
) == dtype_instance.to_native_dtype().type(value)


def test_out_of_bounds_integer_cast_scalar() -> None:
"""cast_scalar raises ValueError for out-of-bounds integers, so
create_array(dtype="i1", fill_value=300) fails the same way reading
stored metadata does."""
with pytest.raises(ValueError, match="out of bounds"):
Int8().cast_scalar(300)
with pytest.raises(ValueError, match="out of bounds"):
UInt64().cast_scalar(2**64)
Loading