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 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_common.py b/tests/test_dtype/test_npy/test_common.py index bd77866fc0..d8912a70ec 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,13 @@ 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") + assert not check_json_intish_float(1.1) + + def test_check_json_str() -> None: assert check_json_str("0") assert not check_json_str(1.0) 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)),