From b1281c44c2ba51088e96f3608a1755bc313d0490 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Thu, 11 Sep 2025 20:57:51 +0200 Subject: [PATCH 1/5] allow int-like floats for int dtype fill values --- src/zarr/core/dtype/npy/common.py | 21 +++++++++++++++++++++ src/zarr/core/dtype/npy/int.py | 3 +++ tests/test_dtype/test_npy/test_int.py | 16 ++++++++-------- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/zarr/core/dtype/npy/common.py b/src/zarr/core/dtype/npy/common.py index 67644449a0..ab22b542f0 100644 --- a/src/zarr/core/dtype/npy/common.py +++ b/src/zarr/core/dtype/npy/common.py @@ -9,6 +9,7 @@ Any, Final, Literal, + NewType, SupportsComplex, SupportsFloat, SupportsIndex, @@ -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 = ">", "<", "=" @@ -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. diff --git a/src/zarr/core/dtype/npy/int.py b/src/zarr/core/dtype/npy/int.py index 01a79142a3..ac04d4469a 100644 --- a/src/zarr/core/dtype/npy/int.py +++ b/src/zarr/core/dtype/npy/int.py @@ -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, ) @@ -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: diff --git a/tests/test_dtype/test_npy/test_int.py b/tests/test_dtype/test_npy/test_int.py index efc4fae496..0cab3b8e3e 100644 --- a/tests/test_dtype/test_npy/test_int.py +++ b/tests/test_dtype/test_npy/test_int.py @@ -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)), @@ -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)), @@ -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)), @@ -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)), @@ -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)), @@ -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)), @@ -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)), @@ -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)), From e4e10e90b148a87b01daa850d12ac79d31f3c033 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 12 Sep 2025 16:04:56 +0200 Subject: [PATCH 2/5] changelog --- changes/3448.bugfix.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changes/3448.bugfix.rst diff --git a/changes/3448.bugfix.rst b/changes/3448.bugfix.rst new file mode 100644 index 0000000000..6c8b106153 --- /dev/null +++ b/changes/3448.bugfix.rst @@ -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. \ No newline at end of file From 101cc047f4651b8e37e3e518e9d3365a7e95db62 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 12 Sep 2025 16:26:16 +0200 Subject: [PATCH 3/5] add specific test for intish float --- tests/test_dtype/test_npy/test_common.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_dtype/test_npy/test_common.py b/tests/test_dtype/test_npy/test_common.py index bd77866fc0..25d29c4b46 100644 --- a/tests/test_dtype/test_npy/test_common.py +++ b/tests/test_dtype/test_npy/test_common.py @@ -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, @@ -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) + 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) From 7d07a374a4bc68b20976779a6c922dac6e336847 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 12 Sep 2025 16:45:16 +0200 Subject: [PATCH 4/5] correct test --- tests/test_dtype/test_npy/test_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_dtype/test_npy/test_common.py b/tests/test_dtype/test_npy/test_common.py index 25d29c4b46..5308364333 100644 --- a/tests/test_dtype/test_npy/test_common.py +++ b/tests/test_dtype/test_npy/test_common.py @@ -322,7 +322,7 @@ def test_check_json_int() -> None: def test_check_json_intish_float() -> None: - assert check_json_intish_float(0) + assert check_json_intish_float(0.0) assert check_json_intish_float(1.0) assert not check_json_intish_float("0") From 38ee2b6476828d8308f444b3351b8b70c4272051 Mon Sep 17 00:00:00 2001 From: Davis Bennett Date: Fri, 12 Sep 2025 16:51:57 +0200 Subject: [PATCH 5/5] Update tests/test_dtype/test_npy/test_common.py Co-authored-by: Ryan Abernathey --- tests/test_dtype/test_npy/test_common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_dtype/test_npy/test_common.py b/tests/test_dtype/test_npy/test_common.py index 5308364333..d8912a70ec 100644 --- a/tests/test_dtype/test_npy/test_common.py +++ b/tests/test_dtype/test_npy/test_common.py @@ -325,6 +325,7 @@ 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") + assert not check_json_intish_float(1.1) def test_check_json_str() -> None: