From 5aad823c88d7763f31489731b21bf8e9b5492303 Mon Sep 17 00:00:00 2001 From: Hauke Schulz <43613877+observingClouds@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:00:35 +0100 Subject: [PATCH 01/12] fix default_fill value handing for strings --- virtualizarr/parsers/zarr.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/virtualizarr/parsers/zarr.py b/virtualizarr/parsers/zarr.py index 37d33f31..6e232e98 100644 --- a/virtualizarr/parsers/zarr.py +++ b/virtualizarr/parsers/zarr.py @@ -8,6 +8,7 @@ import zarr from zarr.api.asynchronous import open_group as open_group_async +from zarr.core.dtype import parse_dtype from zarr.core.group import GroupMetadata from zarr.core.metadata import ArrayV3Metadata from zarr.storage import ObjectStore @@ -194,20 +195,10 @@ def get_metadata(self, zarr_array: ZarrArrayType) -> ArrayV3Metadata: if v2_metadata.fill_value is None: v2_dict = v2_metadata.to_dict() - v2_dict["fill_value"] = 0 + v2_dtype = parse_dtype(v2_dict['dtype'], format=2) + v2_dict["fill_value"] = v2_dtype.default_scalar() temp_v2 = ArrayV2Metadata.from_dict(v2_dict) v3_metadata = _convert_array_metadata(temp_v2) - - # Replace with proper default for the data type - default_scalar = v3_metadata.data_type.default_scalar() - fill_value = ( - default_scalar.item() - if hasattr(default_scalar, "item") - else default_scalar - ) - v3_dict = v3_metadata.to_dict() - v3_dict["fill_value"] = fill_value - v3_metadata = ArrayV3Metadata.from_dict(v3_dict) else: # Normal conversion; allow other errors to propagate. v3_metadata = _convert_array_metadata(v2_metadata) From b32d180c9a45b2d22c2aac21b87b1bcf145f9211 Mon Sep 17 00:00:00 2001 From: Hauke Schulz <43613877+observingClouds@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:03:05 +0100 Subject: [PATCH 02/12] fix syntax --- virtualizarr/parsers/zarr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/virtualizarr/parsers/zarr.py b/virtualizarr/parsers/zarr.py index 6e232e98..045f52ee 100644 --- a/virtualizarr/parsers/zarr.py +++ b/virtualizarr/parsers/zarr.py @@ -195,7 +195,7 @@ def get_metadata(self, zarr_array: ZarrArrayType) -> ArrayV3Metadata: if v2_metadata.fill_value is None: v2_dict = v2_metadata.to_dict() - v2_dtype = parse_dtype(v2_dict['dtype'], format=2) + v2_dtype = parse_dtype(v2_dict['dtype'], zarr_format=2) v2_dict["fill_value"] = v2_dtype.default_scalar() temp_v2 = ArrayV2Metadata.from_dict(v2_dict) v3_metadata = _convert_array_metadata(temp_v2) From 978c6f36545108237e5c9f328043900f5deaeb1a Mon Sep 17 00:00:00 2001 From: Hauke Schulz <43613877+observingClouds@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:11:44 +0100 Subject: [PATCH 03/12] fix dtypes where item method needs to be used --- virtualizarr/parsers/zarr.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/virtualizarr/parsers/zarr.py b/virtualizarr/parsers/zarr.py index 045f52ee..1b0b4ea5 100644 --- a/virtualizarr/parsers/zarr.py +++ b/virtualizarr/parsers/zarr.py @@ -196,7 +196,8 @@ def get_metadata(self, zarr_array: ZarrArrayType) -> ArrayV3Metadata: if v2_metadata.fill_value is None: v2_dict = v2_metadata.to_dict() v2_dtype = parse_dtype(v2_dict['dtype'], zarr_format=2) - v2_dict["fill_value"] = v2_dtype.default_scalar() + fill_value = v2_dtype.default_scalar() + v2_dict["fill_value"] = fill_value.item() if hasattr(fill_value, 'item') else fill_value temp_v2 = ArrayV2Metadata.from_dict(v2_dict) v3_metadata = _convert_array_metadata(temp_v2) else: From 2a375b2e38bc03914787ebe31658811f19992dac Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 16:21:47 +0000 Subject: [PATCH 04/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- virtualizarr/parsers/zarr.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/virtualizarr/parsers/zarr.py b/virtualizarr/parsers/zarr.py index 1b0b4ea5..056ad214 100644 --- a/virtualizarr/parsers/zarr.py +++ b/virtualizarr/parsers/zarr.py @@ -195,9 +195,11 @@ def get_metadata(self, zarr_array: ZarrArrayType) -> ArrayV3Metadata: if v2_metadata.fill_value is None: v2_dict = v2_metadata.to_dict() - v2_dtype = parse_dtype(v2_dict['dtype'], zarr_format=2) + v2_dtype = parse_dtype(v2_dict["dtype"], zarr_format=2) fill_value = v2_dtype.default_scalar() - v2_dict["fill_value"] = fill_value.item() if hasattr(fill_value, 'item') else fill_value + v2_dict["fill_value"] = ( + fill_value.item() if hasattr(fill_value, "item") else fill_value + ) temp_v2 = ArrayV2Metadata.from_dict(v2_dict) v3_metadata = _convert_array_metadata(temp_v2) else: From 1eaca82fc42ff743cb05142bb0b874c824c25d28 Mon Sep 17 00:00:00 2001 From: Hauke Schulz <43613877+observingClouds@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:43:12 +0100 Subject: [PATCH 05/12] add test for several dtypes --- virtualizarr/tests/test_parsers/test_zarr.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/virtualizarr/tests/test_parsers/test_zarr.py b/virtualizarr/tests/test_parsers/test_zarr.py index 6a8bb225..ff374490 100644 --- a/virtualizarr/tests/test_parsers/test_zarr.py +++ b/virtualizarr/tests/test_parsers/test_zarr.py @@ -264,7 +264,17 @@ async def get_meta(): @SKIP_OLDER_ZARR_PYTHON -def test_v2_metadata_with_none_fill_value(): +@pytest.mark.parametrize( + "dtype", + [ + "int32", + "uint8", + "float64", + "bool", + "U10", + ], +) +def test_v2_metadata_with_none_fill_value(dtype): """Test V2 metadata conversion when fill_value is None.""" import asyncio @@ -273,7 +283,7 @@ def test_v2_metadata_with_none_fill_value(): _ = zarr.create( shape=(5, 10), chunks=(5, 5), - dtype="int32", + dtype=dtype, store=store, zarr_format=2, fill_value=None, From c58a5bbcba9a5fbe6f35a9c1b671726c4173eca3 Mon Sep 17 00:00:00 2001 From: Hauke Schulz <43613877+observingClouds@users.noreply.github.com> Date: Thu, 8 Jan 2026 09:24:26 +0100 Subject: [PATCH 06/12] fix mypy type error --- virtualizarr/parsers/zarr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/virtualizarr/parsers/zarr.py b/virtualizarr/parsers/zarr.py index 056ad214..d43b9391 100644 --- a/virtualizarr/parsers/zarr.py +++ b/virtualizarr/parsers/zarr.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from collections.abc import Iterable from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast import zarr from zarr.api.asynchronous import open_group as open_group_async @@ -195,7 +195,7 @@ def get_metadata(self, zarr_array: ZarrArrayType) -> ArrayV3Metadata: if v2_metadata.fill_value is None: v2_dict = v2_metadata.to_dict() - v2_dtype = parse_dtype(v2_dict["dtype"], zarr_format=2) + v2_dtype = parse_dtype(cast(Any, v2_dict["dtype"]), zarr_format=2) fill_value = v2_dtype.default_scalar() v2_dict["fill_value"] = ( fill_value.item() if hasattr(fill_value, "item") else fill_value From bc8c97724e27e2c585f9f3615ef77c4ded1ea1f7 Mon Sep 17 00:00:00 2001 From: Hauke Schulz <43613877+observingClouds@users.noreply.github.com> Date: Thu, 8 Jan 2026 09:45:11 +0100 Subject: [PATCH 07/12] add documentation --- docs/releases.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releases.md b/docs/releases.md index 08f13390..e3f6c192 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -8,6 +8,12 @@ ([809](https://github.com/zarr-developers/VirtualiZarr/pull/809)). By [Julia Signell](https://github.com/jsignell). +### Bug fixes + +- Fix setting `fill_value` for Zarr V2 arrays if data type is a subtype of integer or float. + ([#845](https://github.com/zarr-developers/VirtualiZarr/pull/845)). + By [Hauke Schulz](https://github.com/observingClouds). + ## v2.2.1 (17th November 2025) ### Bug fixes From f2315648e28a35b66d0a1acdb3bdbc2d7159de28 Mon Sep 17 00:00:00 2001 From: Hauke Schulz <43613877+observingClouds@users.noreply.github.com> Date: Mon, 26 Jan 2026 08:24:36 +0100 Subject: [PATCH 08/12] Test additional dtypes; improve fill value conversion Co-authored-by: Max Jones <14077947+maxrjones@users.noreply.github.com> --- virtualizarr/parsers/zarr.py | 4 +--- virtualizarr/tests/test_parsers/test_zarr.py | 4 ++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/virtualizarr/parsers/zarr.py b/virtualizarr/parsers/zarr.py index d43b9391..7e2af66d 100644 --- a/virtualizarr/parsers/zarr.py +++ b/virtualizarr/parsers/zarr.py @@ -197,9 +197,7 @@ def get_metadata(self, zarr_array: ZarrArrayType) -> ArrayV3Metadata: v2_dict = v2_metadata.to_dict() v2_dtype = parse_dtype(cast(Any, v2_dict["dtype"]), zarr_format=2) fill_value = v2_dtype.default_scalar() - v2_dict["fill_value"] = ( - fill_value.item() if hasattr(fill_value, "item") else fill_value - ) + v2_dict["fill_value"] = v2_dtype.to_json_scalar(fill_value, zarr_format=2) temp_v2 = ArrayV2Metadata.from_dict(v2_dict) v3_metadata = _convert_array_metadata(temp_v2) else: diff --git a/virtualizarr/tests/test_parsers/test_zarr.py b/virtualizarr/tests/test_parsers/test_zarr.py index ff374490..070a1f52 100644 --- a/virtualizarr/tests/test_parsers/test_zarr.py +++ b/virtualizarr/tests/test_parsers/test_zarr.py @@ -272,6 +272,10 @@ async def get_meta(): "float64", "bool", "U10", + "datetime64[s]", + "timedelta64[s]", + "S10", + "V10", ], ) def test_v2_metadata_with_none_fill_value(dtype): From 879ad0f26c0c7eb67a01f9ec790b423413b7da8c Mon Sep 17 00:00:00 2001 From: Hauke Schulz <43613877+observingClouds@users.noreply.github.com> Date: Mon, 26 Jan 2026 13:36:25 +0100 Subject: [PATCH 09/12] mypy registry annotation --- virtualizarr/parsers/dmrpp.py | 2 +- virtualizarr/tests/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/virtualizarr/parsers/dmrpp.py b/virtualizarr/parsers/dmrpp.py index 43c6425c..3b36e6a8 100644 --- a/virtualizarr/parsers/dmrpp.py +++ b/virtualizarr/parsers/dmrpp.py @@ -178,7 +178,7 @@ def parse_dataset( ) manifest_group = self._parse_dataset(dataset_element) - registry = ObjectStoreRegistry() + registry: ObjectStoreRegistry = ObjectStoreRegistry() registry.register(self.data_filepath, object_store) return ManifestStore(registry=registry, group=manifest_group) diff --git a/virtualizarr/tests/utils.py b/virtualizarr/tests/utils.py index 73cd26cd..4ce2fece 100644 --- a/virtualizarr/tests/utils.py +++ b/virtualizarr/tests/utils.py @@ -41,7 +41,7 @@ def obstore_http(url: str) -> ObjectStore: def manifest_store_from_hdf_url(url, group: str | None = None): - registry = ObjectStoreRegistry() + registry: ObjectStoreRegistry = ObjectStoreRegistry() registry.register(url, obstore_local(url=url)) parser = HDFParser(group=group) return parser(url=url, registry=registry) From 212903cc6873631d9ed4af6dbbe354bb7a2cf5af Mon Sep 17 00:00:00 2001 From: Hauke Schulz <43613877+observingClouds@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:45:55 +0100 Subject: [PATCH 10/12] implement test for #826 --- .../tests/test_manifests/test_array.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/virtualizarr/tests/test_manifests/test_array.py b/virtualizarr/tests/test_manifests/test_array.py index 631440be..0391d5ff 100644 --- a/virtualizarr/tests/test_manifests/test_array.py +++ b/virtualizarr/tests/test_manifests/test_array.py @@ -131,6 +131,39 @@ def test_equals_nan_fill_value(self, array_v3_metadata): assert result.all() +class TestAstype: + def test_astype_same_dtype(self, manifest_array): + """Test that astype with the same dtype returns self.""" + marr = manifest_array(shape=(5, 10), chunks=(5, 10), data_type=np.dtype("int32")) + result = marr.astype(np.dtype("int32")) + assert result is marr + + def test_astype_string_upcast(self, manifest_array): + """Test that astype allows string dtype upcasting (e.g., Date: Mon, 26 Jan 2026 16:46:47 +0100 Subject: [PATCH 11/12] implement upcasting (fix #826) --- virtualizarr/manifests/array.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/virtualizarr/manifests/array.py b/virtualizarr/manifests/array.py index 0c74077f..967ea986 100644 --- a/virtualizarr/manifests/array.py +++ b/virtualizarr/manifests/array.py @@ -207,7 +207,7 @@ def __eq__( # type: ignore[override] def astype(self, dtype: np.dtype, /, *, copy: bool = True) -> "ManifestArray": """Cannot change the dtype, but needed because xarray will call this even when it's a no-op.""" - if dtype != self.dtype: + if not np.issubdtype(self.dtype, dtype): raise NotImplementedError() else: return self From 9be6948793b1d67b64a0ccd3eeb8ee87337e4b76 Mon Sep 17 00:00:00 2001 From: Hauke Schulz <43613877+observingClouds@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:47:27 +0100 Subject: [PATCH 12/12] apply pre-commit --- virtualizarr/tests/test_manifests/test_array.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/virtualizarr/tests/test_manifests/test_array.py b/virtualizarr/tests/test_manifests/test_array.py index 0391d5ff..c3e3cdfa 100644 --- a/virtualizarr/tests/test_manifests/test_array.py +++ b/virtualizarr/tests/test_manifests/test_array.py @@ -134,7 +134,9 @@ def test_equals_nan_fill_value(self, array_v3_metadata): class TestAstype: def test_astype_same_dtype(self, manifest_array): """Test that astype with the same dtype returns self.""" - marr = manifest_array(shape=(5, 10), chunks=(5, 10), data_type=np.dtype("int32")) + marr = manifest_array( + shape=(5, 10), chunks=(5, 10), data_type=np.dtype("int32") + ) result = marr.astype(np.dtype("int32")) assert result is marr @@ -153,7 +155,9 @@ def test_astype_bytes_upcast(self, manifest_array): def test_astype_incompatible_dtype_raises(self, manifest_array): """Test that astype with incompatible dtype raises NotImplementedError.""" - marr = manifest_array(shape=(5, 10), chunks=(5, 10), data_type=np.dtype("int32")) + marr = manifest_array( + shape=(5, 10), chunks=(5, 10), data_type=np.dtype("int32") + ) with pytest.raises(NotImplementedError): marr.astype(np.dtype("float64"))