Skip to content

Commit d022da5

Browse files
authored
fix: ensure fill_value for zarr v3 arrays round-trips (#11345)
1 parent adc4ebd commit d022da5

3 files changed

Lines changed: 38 additions & 4 deletions

File tree

doc/whats-new.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ Bug Fixes
2929
- Fix a major performance regression in :py:meth:`Coordinates.to_index` (and
3030
consequently :py:meth:`Dataset.to_dataframe`) caused by converting the cached
3131
code ndarrays into Python lists (:issue:`11305`).
32+
- Preserve the Zarr array ``fill_value`` in the variable ``encoding`` when reading
33+
a ``zarr_format=3`` store with ``use_zarr_fill_value_as_mask=False``, so it is no
34+
longer silently lost on round-trip (:issue:`10269`).
35+
By `Davis Bennett <https://github.com/d-v-b>`_.
3236

3337

3438
Documentation

xarray/backends/zarr.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -938,10 +938,19 @@ def open_store_variable(self, name):
938938
# by interpreting Zarr's fill_value to mean the same as netCDF's _FillValue
939939
if zarr_array.fill_value is not None:
940940
attributes["_FillValue"] = zarr_array.fill_value
941-
elif "_FillValue" in attributes:
942-
attributes["_FillValue"] = FillValueCoder.decode(
943-
attributes["_FillValue"], zarr_array.dtype
944-
)
941+
else:
942+
# Preserve the Zarr array fill_value in the encoding so it is not
943+
# lost on round-trip. The write path reads it back from here.
944+
# Only zarr_format 3 supports `fill_value` as an encoding key
945+
# (in zarr_format 2 the fill_value is set via `_FillValue`).
946+
# See https://github.com/pydata/xarray/issues/10269
947+
zarr_format_3 = _zarr_v3() and self.zarr_group.metadata.zarr_format == 3
948+
if zarr_format_3:
949+
encoding["fill_value"] = zarr_array.fill_value
950+
if "_FillValue" in attributes:
951+
attributes["_FillValue"] = FillValueCoder.decode(
952+
attributes["_FillValue"], zarr_array.dtype
953+
)
945954

946955
return Variable(dimensions, data, attributes, encoding)
947956

xarray/tests/test_backends.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3869,6 +3869,27 @@ def test_zarr_fill_value_setting(self, dtype):
38693869
# ``raise_on_invalid=vn in check_encoding_set`` line in zarr.py
38703870
# ds.foo.encoding["fill_value"] = fv
38713871

3872+
def test_zarr_fill_value_in_encoding_on_read(self) -> None:
3873+
# GH #10269: the Zarr array fill_value should be preserved in the
3874+
# variable encoding on read, so that it is not lost on round-trip.
3875+
# `fill_value` is an independent encoding key only for zarr_format 3;
3876+
# for zarr_format 2 the fill_value is set via `_FillValue`.
3877+
if not has_zarr_v3 or zarr.config.get("default_zarr_format") != 3:
3878+
pytest.skip("fill_value is only an encoding key for zarr_format 3")
3879+
3880+
ds = xr.Dataset({"foo": ("x", [1, 2, 3])})
3881+
ds.foo.encoding = {"fill_value": -99}
3882+
3883+
open_kwargs = {"consolidated": False, "use_zarr_fill_value_as_mask": False}
3884+
with self.roundtrip(ds, open_kwargs=open_kwargs) as actual:
3885+
assert actual.foo.encoding["fill_value"] == -99
3886+
3887+
# the fill_value must survive an open -> write -> open round-trip even
3888+
# when the user never touches the encoding explicitly
3889+
with self.roundtrip(ds, open_kwargs=open_kwargs) as opened:
3890+
with self.roundtrip(opened, open_kwargs=open_kwargs) as actual:
3891+
assert actual.foo.encoding["fill_value"] == -99
3892+
38723893

38733894
@requires_zarr
38743895
@pytest.mark.skipif(

0 commit comments

Comments
 (0)