77
88import zarr
99from tests .test_codecs .conftest import Expect , ExpectErr
10- from zarr .codecs .scale_offset import ScaleOffset , _decode , _encode
10+ from zarr .codecs .scale_offset import (
11+ ScaleOffset ,
12+ _decode ,
13+ _decode_fits_natively ,
14+ _encode ,
15+ )
1116from zarr .core .buffer .core import default_buffer_prototype
1217from zarr .storage ._memory import MemoryStore
1318
@@ -124,7 +129,6 @@ def test_construction_accepts_numeric(
124129)
125130def test_encode_decode_roundtrip (dtype : str , offset : float , scale : float ) -> None :
126131 """Data survives encode → decode."""
127- import zarr
128132
129133 arr = zarr .create_array (
130134 store = {},
@@ -142,8 +146,6 @@ def test_encode_decode_roundtrip(dtype: str, offset: float, scale: float) -> Non
142146
143147def test_fill_value_transformed () -> None :
144148 """Fill value is transformed through the encode formula and read back correctly."""
145- import zarr
146-
147149 arr = zarr .create_array (
148150 store = {},
149151 shape = (10 ,),
@@ -178,7 +180,6 @@ def test_identity_is_noop() -> None:
178180
179181def test_rejects_complex_dtype () -> None :
180182 """Complex dtypes are rejected at array creation time."""
181- import zarr
182183
183184 with pytest .raises (ValueError , match = "only supports integer and floating-point" ):
184185 zarr .create_array (
@@ -194,7 +195,6 @@ def test_rejects_complex_dtype() -> None:
194195
195196def test_uint64_large_value_roundtrip () -> None :
196197 """uint64 values above 2**63 must survive encode+decode (spec requires uint64 support)."""
197- import zarr
198198
199199 arr = zarr .create_array (
200200 store = {},
@@ -213,7 +213,6 @@ def test_uint64_large_value_roundtrip() -> None:
213213
214214def test_float_nan_inf_preserved () -> None :
215215 """NaN and Inf are representable in float dtypes per IEEE 754 and must pass through."""
216- from zarr .codecs .scale_offset import _decode , _encode
217216
218217 arr = np .array ([1.0 , np .nan , np .inf , - np .inf ], dtype = "float64" )
219218 encoded = _encode (arr , np .float64 (0.0 ), np .float64 (2.0 ))
@@ -228,7 +227,6 @@ def test_float_nan_inf_preserved() -> None:
228227
229228def test_uint64_encode_rejects_underflow () -> None :
230229 """uint64 underflow during encode raises rather than silently wrapping."""
231- import zarr
232230
233231 arr = zarr .create_array (
234232 store = {},
@@ -245,7 +243,6 @@ def test_uint64_encode_rejects_underflow() -> None:
245243
246244def test_rejects_zero_scale () -> None :
247245 """scale=0 is rejected (destroys data and breaks decode division)."""
248- import zarr
249246
250247 with pytest .raises (ValueError , match = "scale must be non-zero" ):
251248 zarr .create_array (
@@ -412,3 +409,49 @@ async def test_decode_rejects_integer_overflow_on_offset_add() -> None:
412409 await arr .store_path .store .set ("c/0" , buf )
413410 with pytest .raises (ValueError , match = "outside the range of dtype int8" ):
414411 arr [:]
412+
413+
414+ def test_decode_fits_natively_negative_scale () -> None :
415+ """_decode_fits_natively handles negative scale by swapping bounds."""
416+ # For a negative scale, x // scale flips the relationship between min/max.
417+ # The function should use info.max // scale as the lower bound and info.min // scale
418+ # as the upper bound.
419+ dtype = np .dtype ("int16" )
420+ # scale=-2 inverts; offset=0 means range is just q_lo..q_hi
421+ assert _decode_fits_natively (dtype , offset = 0 , scale = - 2 ) is True
422+ # An offset that pushes the range out of bounds returns False
423+ assert _decode_fits_natively (dtype , offset = 100000 , scale = - 2 ) is False
424+
425+
426+ async def test_decode_int_widened_path () -> None :
427+ """When _decode_fits_natively returns False, decode falls through to the widened path."""
428+ # For uint32 with offset near max, q_hi + offset can exceed uint32 if computed in target dtype.
429+ # The widened path uses int64 arithmetic and range-checks the result.
430+ # We bypass encode by writing raw bytes directly to the store.
431+ store = MemoryStore ()
432+ arr = zarr .create_array (
433+ store = store ,
434+ shape = (3 ,),
435+ dtype = "uint32" ,
436+ chunks = (3 ,),
437+ # offset large enough that _decode_fits_natively returns False
438+ filters = [ScaleOffset (offset = 2 ** 31 , scale = 1 )],
439+ compressors = None ,
440+ # fill_value must be >= offset to avoid uint32 underflow during encode
441+ fill_value = 2 ** 31 ,
442+ )
443+ # Encoded values that, when added to offset, stay within uint32
444+ buf = default_buffer_prototype ().buffer .from_bytes (
445+ np .array ([0 , 100 , 1000 ], dtype = "uint32" ).tobytes ()
446+ )
447+ await arr .store_path .store .set ("c/0" , buf )
448+ expected = np .array ([2 ** 31 , 2 ** 31 + 100 , 2 ** 31 + 1000 ], dtype = "uint32" )
449+ np .testing .assert_array_equal (arr [:], expected )
450+
451+
452+ def test_compute_encoded_size () -> None :
453+ """compute_encoded_size returns the input byte length unchanged (codec is fixed-size)."""
454+ codec = ScaleOffset (offset = 0 , scale = 1 )
455+ # The chunk_spec argument is unused; pass any sentinel
456+ assert codec .compute_encoded_size (input_byte_length = 100 , _chunk_spec = None ) == 100 # type: ignore[arg-type]
457+ assert codec .compute_encoded_size (input_byte_length = 0 , _chunk_spec = None ) == 0 # type: ignore[arg-type]
0 commit comments