From 791bfec3bf2579d062aca3df20cfa3319adef110 Mon Sep 17 00:00:00 2001 From: Max Jones <14077947+maxrjones@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:29:14 -0500 Subject: [PATCH 1/8] Support encoding/decoding complex _FillValue in Zarr --- xarray/backends/zarr.py | 24 ++++++++++++++++++++++-- xarray/tests/test_backends.py | 12 ++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/xarray/backends/zarr.py b/xarray/backends/zarr.py index 03387051b3b..7162892eca2 100644 --- a/xarray/backends/zarr.py +++ b/xarray/backends/zarr.py @@ -122,7 +122,9 @@ class FillValueCoder: """ @classmethod - def encode(cls, value: int | float | str | bytes, dtype: np.dtype[Any]) -> Any: + def encode( + cls, value: int | float | complex | str | bytes, dtype: np.dtype[Any] + ) -> Any: if dtype.kind in "S": # byte string, this implies that 'value' must also be `bytes` dtype. assert isinstance(value, bytes) @@ -135,13 +137,25 @@ def encode(cls, value: int | float | str | bytes, dtype: np.dtype[Any]) -> Any: return int(value) elif dtype.kind in "f": return base64.standard_b64encode(struct.pack(" None: assert actual3 == expected3 +@requires_zarr +@pytest.mark.parametrize("dtype", [np.complex64, np.complex128]) +def test_fill_value_coder_complex(dtype) -> None: + """Test that FillValueCoder round-trips complex fill values.""" + from xarray.backends.zarr import FillValueCoder + + for value in [dtype(1 + 2j), dtype(-3.5 + 4.5j), dtype(complex("nan+nanj"))]: + encoded = FillValueCoder.encode(value, np.dtype(dtype)) + decoded = FillValueCoder.decode(encoded, np.dtype(dtype)) + np.testing.assert_equal(np.array(decoded, dtype=dtype), np.array(value)) + + @requires_zarr def test_extract_zarr_variable_encoding() -> None: var = xr.Variable("x", [1, 2]) From 83e0413c612521e12c87adde5f8ddaf1a2e52e6e Mon Sep 17 00:00:00 2001 From: Max Jones <14077947+maxrjones@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:47:52 -0500 Subject: [PATCH 2/8] Address typing error --- xarray/backends/zarr.py | 1 + 1 file changed, 1 insertion(+) diff --git a/xarray/backends/zarr.py b/xarray/backends/zarr.py index 7162892eca2..90ccce343be 100644 --- a/xarray/backends/zarr.py +++ b/xarray/backends/zarr.py @@ -139,6 +139,7 @@ def encode( return base64.standard_b64encode(struct.pack(" Date: Tue, 10 Feb 2026 12:07:04 -0500 Subject: [PATCH 3/8] Fix assertion --- xarray/backends/zarr.py | 4 +++- xarray/tests/test_backends.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/xarray/backends/zarr.py b/xarray/backends/zarr.py index 90ccce343be..98a1708ced6 100644 --- a/xarray/backends/zarr.py +++ b/xarray/backends/zarr.py @@ -139,7 +139,9 @@ def encode( return base64.standard_b64encode(struct.pack(" None: @requires_zarr -@pytest.mark.parametrize("dtype", [np.complex64, np.complex128]) +@pytest.mark.parametrize("dtype", [complex, np.complex64, np.complex128]) def test_fill_value_coder_complex(dtype) -> None: """Test that FillValueCoder round-trips complex fill values.""" from xarray.backends.zarr import FillValueCoder From 932269869f645e9f1bfd5cbbd3b1580083fd8629 Mon Sep 17 00:00:00 2001 From: Deepak Cherian Date: Fri, 13 Feb 2026 09:53:13 -0700 Subject: [PATCH 4/8] Fix types --- xarray/backends/zarr.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/xarray/backends/zarr.py b/xarray/backends/zarr.py index 98a1708ced6..8abe0088cd1 100644 --- a/xarray/backends/zarr.py +++ b/xarray/backends/zarr.py @@ -134,8 +134,10 @@ def encode( return bool(value) elif dtype.kind in "iu": # todo: do we want to check for decimals? + assert isinstance(value, int | float) return int(value) elif dtype.kind in "f": + assert isinstance(value, int | float) return base64.standard_b64encode(struct.pack(" Date: Wed, 8 Apr 2026 19:56:54 -0400 Subject: [PATCH 5/8] Add roundtrip test --- xarray/backends/zarr.py | 6 +++--- xarray/tests/test_backends.py | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/xarray/backends/zarr.py b/xarray/backends/zarr.py index e2eec14e73e..4c4cf98aef9 100644 --- a/xarray/backends/zarr.py +++ b/xarray/backends/zarr.py @@ -134,10 +134,10 @@ def encode( return bool(value) elif dtype.kind in "iu": # todo: do we want to check for decimals? - assert isinstance(value, int | float) + assert isinstance(value, int | float | np.integer | np.floating) return int(value) elif dtype.kind == "f": - assert isinstance(value, int | float) + assert isinstance(value, int | float | np.integer | np.floating) return base64.standard_b64encode(struct.pack(" None: np.testing.assert_equal(np.array(decoded, dtype=dtype), np.array(value)) +@requires_zarr +@pytest.mark.parametrize( + "value,dtype", + [ + (np.float32(np.inf), np.float32), + (np.float32(-np.inf), np.float32), + (np.float64(np.inf), np.float64), + (np.float64(-np.inf), np.float64), + (np.float32(np.nan), np.float32), + (np.float64(np.nan), np.float64), + ], +) +def test_fill_value_coder_inf_nan(value, dtype) -> None: + """Test that FillValueCoder round-trips inf and nan fill values.""" + from xarray.backends.zarr import FillValueCoder + + encoded = FillValueCoder.encode(value, np.dtype(dtype)) + decoded = FillValueCoder.decode(encoded, np.dtype(dtype)) + np.testing.assert_equal( + np.array(decoded, dtype=dtype), np.array(value, dtype=dtype) + ) + + @requires_zarr def test_extract_zarr_variable_encoding() -> None: var = xr.Variable("x", [1, 2]) From 4d0f70e06f54c5839aa48c297ee79a4acf12b853 Mon Sep 17 00:00:00 2001 From: Max Jones <14077947+maxrjones@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:16:05 -0400 Subject: [PATCH 6/8] Add property test --- properties/test_encode_decode.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/properties/test_encode_decode.py b/properties/test_encode_decode.py index 87bbfdba933..bce2fc215c6 100644 --- a/properties/test_encode_decode.py +++ b/properties/test_encode_decode.py @@ -15,6 +15,7 @@ import hypothesis.extra.numpy as npst import numpy as np from hypothesis import given +from hypothesis import strategies as st import xarray as xr from xarray.coding.times import _parse_iso8601 @@ -48,6 +49,22 @@ def test_CFScaleOffset_coder_roundtrip(original) -> None: xr.testing.assert_identical(original, roundtripped) +@given( + real=st.floats(allow_nan=True, allow_infinity=True), + imag=st.floats(allow_nan=True, allow_infinity=True), + dtype=st.sampled_from([np.complex64, np.complex128]), +) +def test_FillValueCoder_complex_roundtrip(real, imag, dtype) -> None: + from xarray.backends.zarr import FillValueCoder + + value = dtype(complex(real, imag)) + encoded = FillValueCoder.encode(value, np.dtype(dtype)) + decoded = FillValueCoder.decode(encoded, np.dtype(dtype)) + np.testing.assert_equal( + np.array(decoded, dtype=dtype), np.array(value, dtype=dtype) + ) + + @given(dt=datetimes()) def test_iso8601_decode(dt): iso = dt.isoformat() From d411d44c29aa0379abf1f5ab285c33c7280f3ea5 Mon Sep 17 00:00:00 2001 From: Max Jones <14077947+maxrjones@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:25:54 -0400 Subject: [PATCH 7/8] Raise informative errors --- xarray/backends/zarr.py | 43 +++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/xarray/backends/zarr.py b/xarray/backends/zarr.py index 4c4cf98aef9..d9279dc2de9 100644 --- a/xarray/backends/zarr.py +++ b/xarray/backends/zarr.py @@ -127,23 +127,34 @@ def encode( ) -> Any: if dtype.kind == "S": # byte string, this implies that 'value' must also be `bytes` dtype. - assert isinstance(value, bytes) + if not isinstance(value, bytes): + raise TypeError( + f"Failed to encode fill_value: expected bytes for dtype {dtype}, got {type(value).__name__}" + ) return base64.standard_b64encode(value).decode() elif dtype.kind == "b": # boolean return bool(value) elif dtype.kind in "iu": - # todo: do we want to check for decimals? - assert isinstance(value, int | float | np.integer | np.floating) + if not isinstance(value, int | float | np.integer | np.floating): + raise TypeError( + f"Failed to encode fill_value: expected int or float for dtype {dtype}, got {type(value).__name__}" + ) return int(value) elif dtype.kind == "f": - assert isinstance(value, int | float | np.integer | np.floating) + if not isinstance(value, int | float | np.integer | np.floating): + raise TypeError( + f"Failed to encode fill_value: expected int or float for dtype {dtype}, got {type(value).__name__}" + ) return base64.standard_b64encode(struct.pack(" Date: Thu, 9 Apr 2026 12:28:36 -0400 Subject: [PATCH 8/8] Add changelog --- doc/whats-new.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 15cfb8e5d81..9755cd5ea34 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -17,6 +17,8 @@ New Features - Added ``inherit='all_coords'`` option to :py:meth:`DataTree.to_dataset` to inherit all parent coordinates, not just indexed ones (:issue:`10812`, :pull:`11230`). By `Alfonso Ladino `_. +- Added complex dtype support to FillValueCoder for the Zarr backend. (:pull:`11151`) + By `Max Jones `_. Breaking Changes ~~~~~~~~~~~~~~~~