Skip to content

Commit cdb2130

Browse files
d-v-bclaude
andauthored
Fix unreachable match arms in parse_dtype_v3 (float64/complex64 unsupported) (#167)
* Fix unreachable match arms in parse_dtype_v3 (float64/complex64) Two match arms in `parse_dtype_v3` (in both `pydantic_zarr.v3` and `pydantic_zarr.experimental.v3`) were copy-paste errors: the third arm used `Float16DType` instead of `Float64DType`, and the fourth used `Float32DType` instead of `Complex64DType`. This made `float64` and `complex64` dtypes unreachable, causing `ValueError: Unsupported dtype` for those types. Fixes part of #165. Also fixes the test conftest to handle zarr 3.2.1's new `Struct` dtype class (a subclass of `Structured`) by using `issubclass` instead of `==` in the special-case branch, and bumps the mypy `python_version` to 3.12 to match zarr >=3.2.0's minimum Python requirement. Adds parametrized regression tests covering all 13 supported numpy dtypes through `parse_dtype_v3` in both test suites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add changelog fragment for PR #167 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Move zarr 3.2.x compat changes to dedicated PR #171 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * make tests pass * import struct under conditional * robust import * pin pytest and fix iterator issue in test fixtures --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent f1214af commit cdb2130

8 files changed

Lines changed: 81 additions & 12 deletions

File tree

changes/167.bugfix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed unreachable match arms in `parse_dtype_v3` (in both `pydantic_zarr.v3` and `pydantic_zarr.experimental.v3`) where copy-paste errors caused `float64` and `complex64` numpy dtypes to raise `ValueError: Unsupported dtype` instead of returning the correct string names.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,9 @@ Source = "https://github.com/zarr-developers/pydantic-zarr"
3232
zarr = ["zarr>=3.0.0"]
3333

3434
[dependency-groups]
35-
# pytest pin is due to https://github.com/pytest-dev/pytest-cov/issues/693
3635
test-base = [
3736
"coverage",
38-
"pytest<8.4",
37+
"pytest==9.1.0",
3938
"pytest-cov",
4039
"pytest-examples",
4140
"xarray==2025.10.0",
@@ -46,6 +45,7 @@ test = [
4645
"pydantic-zarr[zarr]",
4746
]
4847
docs = [
48+
{include-group = "test-base"},
4949
"mkdocs-material",
5050
"mkdocstrings[python]",
5151
"pytest-examples",

src/pydantic_zarr/experimental/v3.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,9 @@ def parse_dtype_v3(dtype: npt.DTypeLike | Mapping[str, object]) -> Mapping[str,
182182
return "float16"
183183
case np.dtypes.Float32DType():
184184
return "float32"
185-
case np.dtypes.Float16DType():
185+
case np.dtypes.Float64DType():
186186
return "float64"
187-
case np.dtypes.Float32DType():
187+
case np.dtypes.Complex64DType():
188188
return "complex64"
189189
case np.dtypes.Complex128DType():
190190
return "complex128"

src/pydantic_zarr/v3.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,17 @@
6262
FloatFillValue = Literal["Infinity", "-Infinity", "NaN"] | float
6363
ComplexFillValue = tuple[FloatFillValue, FloatFillValue]
6464
RawFillValue = tuple[int, ...]
65-
66-
FillValue = BoolFillValue | IntFillValue | FloatFillValue | ComplexFillValue | RawFillValue | str
65+
StructFillValue = Mapping[str, object]
66+
67+
FillValue = (
68+
BoolFillValue
69+
| IntFillValue
70+
| FloatFillValue
71+
| ComplexFillValue
72+
| RawFillValue
73+
| str
74+
| StructFillValue
75+
)
6776

6877
TName = TypeVar("TName", bound=str)
6978
TConfig = TypeVar("TConfig", bound=Mapping[str, object])
@@ -161,9 +170,9 @@ def parse_dtype_v3(dtype: npt.DTypeLike | Mapping[str, object]) -> Mapping[str,
161170
return "float16"
162171
case np.dtypes.Float32DType():
163172
return "float32"
164-
case np.dtypes.Float16DType():
173+
case np.dtypes.Float64DType():
165174
return "float64"
166-
case np.dtypes.Float32DType():
175+
case np.dtypes.Complex64DType():
167176
return "complex64"
168177
case np.dtypes.Complex128DType():
169178
return "complex128"

tests/test_docs/test_docs.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
SOURCES_ROOT: Path = Path(__file__).parent.parent.parent / "src/pydantic_zarr"
99

1010

11-
@pytest.mark.parametrize("example", find_examples(str(SOURCES_ROOT)), ids=str)
11+
@pytest.mark.parametrize("example", tuple(find_examples(str(SOURCES_ROOT))), ids=str)
1212
def test_docstrings(example: CodeExample, eval_example: EvalExample) -> None:
1313
eval_example.run_print_check(example)
1414

1515

16-
@pytest.mark.parametrize("example", find_examples("docs"), ids=str)
16+
@pytest.mark.parametrize("example", tuple(find_examples("docs")), ids=str)
1717
def test_docs_examples(example: CodeExample, eval_example: EvalExample) -> None:
1818
pytest.importorskip("zarr")
1919

tests/test_pydantic_zarr/conftest.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ class DTypeExample:
7373
Int32,
7474
NullTerminatedBytes,
7575
RawBytes,
76-
Structured,
7776
TimeDelta64,
7877
data_type_registry,
7978
)
@@ -85,7 +84,7 @@ class DTypeExample:
8584
dt = dtype_cls(unit="s", scale_factor=10)
8685
elif dtype_cls in (FixedLengthUTF32, RawBytes, NullTerminatedBytes):
8786
dt = dtype_cls(length=10)
88-
elif dtype_cls == Structured:
87+
elif dtype_cls._zarr_v3_name in ("struct", "structured"):
8988
dt = dtype_cls(fields=[("a", Int32()), ("b", Float16())])
9089
else:
9190
dt = dtype_cls()

tests/test_pydantic_zarr/test_experimental/test_v3.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
RegularChunking,
2121
RegularChunkingConfig,
2222
auto_codecs,
23+
parse_dtype_v3,
2324
)
2425

2526
from ..conftest import DTYPE_EXAMPLES_V3, ZARR_AVAILABLE, DTypeExample
@@ -479,6 +480,35 @@ def test_consolidated_metadata_to_from_zarr() -> None:
479480
assert json.loads(store["zarr.json"].to_bytes()) == json.loads(store2["zarr.json"].to_bytes())
480481

481482

483+
@pytest.mark.parametrize(
484+
("dtype", "expected"),
485+
[
486+
(np.dtype("int8"), "int8"),
487+
(np.dtype("int16"), "int16"),
488+
(np.dtype("int32"), "int32"),
489+
(np.dtype("int64"), "int64"),
490+
(np.dtype("uint8"), "uint8"),
491+
(np.dtype("uint16"), "uint16"),
492+
(np.dtype("uint32"), "uint32"),
493+
(np.dtype("uint64"), "uint64"),
494+
(np.dtype("float16"), "float16"),
495+
(np.dtype("float32"), "float32"),
496+
(np.dtype("float64"), "float64"),
497+
(np.dtype("complex64"), "complex64"),
498+
(np.dtype("complex128"), "complex128"),
499+
],
500+
ids=str,
501+
)
502+
def test_parse_dtype_v3_numpy(dtype: np.dtype, expected: str) -> None:
503+
"""
504+
Regression test: parse_dtype_v3 must correctly handle all supported numpy dtypes.
505+
Previously, the float64 and complex64 match arms were copy-paste errors (using
506+
Float16DType and Float32DType respectively), making those dtypes unreachable and
507+
causing ValueError to be raised for float64 and complex64 inputs.
508+
"""
509+
assert parse_dtype_v3(dtype) == expected
510+
511+
482512
def test_v2_chunk_key_encoding() -> None:
483513
# Simple smoke test to make sure v2 chunk key encoding is allowed
484514
ArraySpec(

tests/test_pydantic_zarr/test_v3.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
RegularChunking,
2323
RegularChunkingConfig,
2424
auto_codecs,
25+
parse_dtype_v3,
2526
)
2627

2728
from .conftest import DTYPE_EXAMPLES_V3, DTypeExample
@@ -313,3 +314,32 @@ def test_v2_chunk_key_encoding() -> None:
313314
fill_value="NaN",
314315
storage_transformers=[],
315316
)
317+
318+
319+
@pytest.mark.parametrize(
320+
("dtype", "expected"),
321+
[
322+
(np.dtype("int8"), "int8"),
323+
(np.dtype("int16"), "int16"),
324+
(np.dtype("int32"), "int32"),
325+
(np.dtype("int64"), "int64"),
326+
(np.dtype("uint8"), "uint8"),
327+
(np.dtype("uint16"), "uint16"),
328+
(np.dtype("uint32"), "uint32"),
329+
(np.dtype("uint64"), "uint64"),
330+
(np.dtype("float16"), "float16"),
331+
(np.dtype("float32"), "float32"),
332+
(np.dtype("float64"), "float64"),
333+
(np.dtype("complex64"), "complex64"),
334+
(np.dtype("complex128"), "complex128"),
335+
],
336+
ids=str,
337+
)
338+
def test_parse_dtype_v3_numpy(dtype: np.dtype, expected: str) -> None:
339+
"""
340+
Regression test: parse_dtype_v3 must correctly handle all supported numpy dtypes.
341+
Previously, the float64 and complex64 match arms were copy-paste errors (using
342+
Float16DType and Float32DType respectively), making those dtypes unreachable and
343+
causing ValueError to be raised for float64 and complex64 inputs.
344+
"""
345+
assert parse_dtype_v3(dtype) == expected

0 commit comments

Comments
 (0)