From 5cc034d8d98532ec84dea2013be37e4da2250846 Mon Sep 17 00:00:00 2001 From: Cipher Date: Wed, 11 Mar 2026 01:58:33 -0700 Subject: [PATCH 1/2] fix: BaseFloat._check_scalar rejects invalid string values (#3586) BaseFloat._check_scalar returned True for all strings because the FloatLike type union includes str. This allowed invalid strings like 'not valid' to pass the check and then raise a confusing ValueError in _cast_scalar_unchecked instead of the expected TypeError. Fix _check_scalar to validate string inputs by attempting conversion: - Valid float strings (e.g. 'NaN', 'inf', '-inf', '1.5') return True - Invalid strings (e.g. 'not valid') return False, causing cast_scalar to raise TypeError as expected Add test cases for invalid string inputs to invalid_scalar_params. Fixes #3586 --- src/zarr/core/dtype/npy/float.py | 9 +++++++++ tests/test_dtype/test_npy/test_float.py | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/zarr/core/dtype/npy/float.py b/src/zarr/core/dtype/npy/float.py index 0be2cbca9b..775848ca6f 100644 --- a/src/zarr/core/dtype/npy/float.py +++ b/src/zarr/core/dtype/npy/float.py @@ -201,6 +201,15 @@ def _check_scalar(self, data: object) -> TypeGuard[FloatLike]: TypeGuard[FloatLike] True if the input is a valid scalar value, False otherwise. """ + if isinstance(data, str): + # Only accept strings that are valid float representations (e.g. "NaN", "inf"). + # Plain strings that cannot be converted should return False so that cast_scalar + # raises TypeError rather than a confusing ValueError. + try: + self.to_native_dtype().type(data) + return True + except (ValueError, OverflowError): + return False return isinstance(data, FloatLike) def _cast_scalar_unchecked(self, data: FloatLike) -> TFloatScalar_co: diff --git a/tests/test_dtype/test_npy/test_float.py b/tests/test_dtype/test_npy/test_float.py index 1bbcbbc81f..feee10c1b4 100644 --- a/tests/test_dtype/test_npy/test_float.py +++ b/tests/test_dtype/test_npy/test_float.py @@ -65,7 +65,7 @@ class TestFloat16(_BaseTestFloat): (Float16(), -1.0, np.float16(-1.0)), (Float16(), "NaN", np.float16("NaN")), ) - invalid_scalar_params = ((Float16(), {"set!"}),) + invalid_scalar_params = ((Float16(), {"set!"}), (Float16(), "not_a_float"),) hex_string_params = (("0x7fc0", np.nan), ("0x7fc1", np.nan), ("0x3c00", 1.0)) item_size_params = (Float16(),) @@ -113,7 +113,7 @@ class TestFloat32(_BaseTestFloat): (Float32(), -1.0, np.float32(-1.0)), (Float32(), "NaN", np.float32("NaN")), ) - invalid_scalar_params = ((Float32(), {"set!"}),) + invalid_scalar_params = ((Float32(), {"set!"}), (Float32(), "not_a_float"),) hex_string_params = (("0x7fc00000", np.nan), ("0x7fc00001", np.nan), ("0x3f800000", 1.0)) item_size_params = (Float32(),) @@ -160,7 +160,7 @@ class TestFloat64(_BaseTestFloat): (Float64(), -1.0, np.float64(-1.0)), (Float64(), "NaN", np.float64("NaN")), ) - invalid_scalar_params = ((Float64(), {"set!"}),) + invalid_scalar_params = ((Float64(), {"set!"}), (Float64(), "not_a_float"),) hex_string_params = ( ("0x7ff8000000000000", np.nan), ("0x7ff8000000000001", np.nan), From c7ea02b93c64eb74cd386f96f662c15892fa782d Mon Sep 17 00:00:00 2001 From: Cipher Date: Wed, 11 Mar 2026 02:26:19 -0700 Subject: [PATCH 2/2] style: fix ruff formatting and TRY300 in _check_scalar --- src/zarr/core/dtype/npy/float.py | 3 ++- tests/test_dtype/test_npy/test_float.py | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/zarr/core/dtype/npy/float.py b/src/zarr/core/dtype/npy/float.py index 775848ca6f..2a23cb429d 100644 --- a/src/zarr/core/dtype/npy/float.py +++ b/src/zarr/core/dtype/npy/float.py @@ -207,9 +207,10 @@ def _check_scalar(self, data: object) -> TypeGuard[FloatLike]: # raises TypeError rather than a confusing ValueError. try: self.to_native_dtype().type(data) - return True except (ValueError, OverflowError): return False + else: + return True return isinstance(data, FloatLike) def _cast_scalar_unchecked(self, data: FloatLike) -> TFloatScalar_co: diff --git a/tests/test_dtype/test_npy/test_float.py b/tests/test_dtype/test_npy/test_float.py index feee10c1b4..8d8e768263 100644 --- a/tests/test_dtype/test_npy/test_float.py +++ b/tests/test_dtype/test_npy/test_float.py @@ -65,7 +65,10 @@ class TestFloat16(_BaseTestFloat): (Float16(), -1.0, np.float16(-1.0)), (Float16(), "NaN", np.float16("NaN")), ) - invalid_scalar_params = ((Float16(), {"set!"}), (Float16(), "not_a_float"),) + invalid_scalar_params = ( + (Float16(), {"set!"}), + (Float16(), "not_a_float"), + ) hex_string_params = (("0x7fc0", np.nan), ("0x7fc1", np.nan), ("0x3c00", 1.0)) item_size_params = (Float16(),) @@ -113,7 +116,10 @@ class TestFloat32(_BaseTestFloat): (Float32(), -1.0, np.float32(-1.0)), (Float32(), "NaN", np.float32("NaN")), ) - invalid_scalar_params = ((Float32(), {"set!"}), (Float32(), "not_a_float"),) + invalid_scalar_params = ( + (Float32(), {"set!"}), + (Float32(), "not_a_float"), + ) hex_string_params = (("0x7fc00000", np.nan), ("0x7fc00001", np.nan), ("0x3f800000", 1.0)) item_size_params = (Float32(),) @@ -160,7 +166,10 @@ class TestFloat64(_BaseTestFloat): (Float64(), -1.0, np.float64(-1.0)), (Float64(), "NaN", np.float64("NaN")), ) - invalid_scalar_params = ((Float64(), {"set!"}), (Float64(), "not_a_float"),) + invalid_scalar_params = ( + (Float64(), {"set!"}), + (Float64(), "not_a_float"), + ) hex_string_params = ( ("0x7ff8000000000000", np.nan), ("0x7ff8000000000001", np.nan),