Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 3 additions & 0 deletions changes/3448.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Setting ``fill_value`` to a float like ``0.0`` when the data type of the array is an integer is a common
mistake. This change lets Zarr Python read arrays with this erroneous metadata, although Zarr Python
will not create such arrays.
21 changes: 21 additions & 0 deletions src/zarr/core/dtype/npy/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
Any,
Final,
Literal,
NewType,
SupportsComplex,
SupportsFloat,
SupportsIndex,
Expand Down Expand Up @@ -54,6 +55,9 @@
"generic",
)

IntishFloat = NewType("IntishFloat", float)
"""A type for floats that represent integers, like 1.0 (but not 1.1)."""

NumpyEndiannessStr = Literal[">", "<", "="]
NUMPY_ENDIANNESS_STR: Final = ">", "<", "="

Expand Down Expand Up @@ -467,6 +471,23 @@ def check_json_int(data: JSON) -> TypeGuard[int]:
return bool(isinstance(data, int))


def check_json_intish_float(data: JSON) -> TypeGuard[IntishFloat]:
"""
Check if a JSON value is an "intish float", i.e. a float that represents an integer, like 0.0.

Parameters
----------
data : JSON
The JSON value to check.

Returns
-------
Bool
True if the data is an intish float, False otherwise.
"""
return isinstance(data, float) and data.is_integer()


def check_json_str(data: JSON) -> TypeGuard[str]:
"""
Check if a JSON value is a string.
Expand Down
3 changes: 3 additions & 0 deletions src/zarr/core/dtype/npy/int.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
)
from zarr.core.dtype.npy.common import (
check_json_int,
check_json_intish_float,
endianness_to_numpy_str,
get_endianness_from_numpy_dtype,
)
Expand Down Expand Up @@ -206,6 +207,8 @@ def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> TIntScalar
"""
if check_json_int(data):
return self._cast_scalar_unchecked(data)
if check_json_intish_float(data):
return self._cast_scalar_unchecked(int(data))
raise TypeError(f"Invalid type: {data}. Expected an integer.")

def to_json_scalar(self, data: object, *, zarr_format: ZarrFormat) -> int:
Expand Down
7 changes: 7 additions & 0 deletions tests/test_dtype/test_npy/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
check_json_float_v2,
check_json_float_v3,
check_json_int,
check_json_intish_float,
check_json_str,
complex_float_to_json_v2,
complex_float_to_json_v3,
Expand Down Expand Up @@ -320,6 +321,12 @@ def test_check_json_int() -> None:
assert not check_json_int(1.0)


def test_check_json_intish_float() -> None:
assert check_json_intish_float(0.0)
assert check_json_intish_float(1.0)
assert not check_json_intish_float("0")


def test_check_json_str() -> None:
assert check_json_str("0")
assert not check_json_str(1.0)
Expand Down
16 changes: 8 additions & 8 deletions tests/test_dtype/test_npy/test_int.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class TestInt8(BaseTestZDType):
{"name": "int8", "configuration": {"endianness": "little"}},
)

scalar_v2_params = ((Int8(), 1), (Int8(), -1))
scalar_v2_params = ((Int8(), 1), (Int8(), -1), (Int8(), 1.0))
scalar_v3_params = ((Int8(), 1), (Int8(), -1))
cast_value_params = (
(Int8(), 1, np.int8(1)),
Expand Down Expand Up @@ -63,7 +63,7 @@ class TestInt16(BaseTestZDType):
{"name": "int16", "configuration": {"endianness": "little"}},
)

scalar_v2_params = ((Int16(), 1), (Int16(), -1))
scalar_v2_params = ((Int16(), 1), (Int16(), -1), (Int16(), 1.0))
scalar_v3_params = ((Int16(), 1), (Int16(), -1))
cast_value_params = (
(Int16(), 1, np.int16(1)),
Expand Down Expand Up @@ -101,7 +101,7 @@ class TestInt32(BaseTestZDType):
{"name": "int32", "configuration": {"endianness": "little"}},
)

scalar_v2_params = ((Int32(), 1), (Int32(), -1))
scalar_v2_params = ((Int32(), 1), (Int32(), -1), (Int32(), 1.0))
scalar_v3_params = ((Int32(), 1), (Int32(), -1))
cast_value_params = (
(Int32(), 1, np.int32(1)),
Expand Down Expand Up @@ -136,7 +136,7 @@ class TestInt64(BaseTestZDType):
{"name": "int64", "configuration": {"endianness": "little"}},
)

scalar_v2_params = ((Int64(), 1), (Int64(), -1))
scalar_v2_params = ((Int64(), 1), (Int64(), -1), (Int64(), 1.0))
scalar_v3_params = ((Int64(), 1), (Int64(), -1))
cast_value_params = (
(Int64(), 1, np.int64(1)),
Expand Down Expand Up @@ -168,7 +168,7 @@ class TestUInt8(BaseTestZDType):
{"name": "uint8", "configuration": {"endianness": "little"}},
)

scalar_v2_params = ((UInt8(), 1), (UInt8(), 0))
scalar_v2_params = ((UInt8(), 1), (UInt8(), 0), (UInt8(), 1.0))
scalar_v3_params = ((UInt8(), 1), (UInt8(), 0))
cast_value_params = (
(UInt8(), 1, np.uint8(1)),
Expand Down Expand Up @@ -203,7 +203,7 @@ class TestUInt16(BaseTestZDType):
{"name": "uint16", "configuration": {"endianness": "little"}},
)

scalar_v2_params = ((UInt16(), 1), (UInt16(), 0))
scalar_v2_params = ((UInt16(), 1), (UInt16(), 0), (UInt16(), 1.0))
scalar_v3_params = ((UInt16(), 1), (UInt16(), 0))
cast_value_params = (
(UInt16(), 1, np.uint16(1)),
Expand Down Expand Up @@ -238,7 +238,7 @@ class TestUInt32(BaseTestZDType):
{"name": "uint32", "configuration": {"endianness": "little"}},
)

scalar_v2_params = ((UInt32(), 1), (UInt32(), 0))
scalar_v2_params = ((UInt32(), 1), (UInt32(), 0), (UInt32(), 1.0))
scalar_v3_params = ((UInt32(), 1), (UInt32(), 0))
cast_value_params = (
(UInt32(), 1, np.uint32(1)),
Expand Down Expand Up @@ -273,7 +273,7 @@ class TestUInt64(BaseTestZDType):
{"name": "uint64", "configuration": {"endianness": "little"}},
)

scalar_v2_params = ((UInt64(), 1), (UInt64(), 0))
scalar_v2_params = ((UInt64(), 1), (UInt64(), 0), (UInt64(), 1.0))
scalar_v3_params = ((UInt64(), 1), (UInt64(), 0))
cast_value_params = (
(UInt64(), 1, np.uint64(1)),
Expand Down
Loading