diff --git a/changes/4414.bugfix.md b/changes/4414.bugfix.md new file mode 100644 index 0000000000..ab15e31b61 --- /dev/null +++ b/changes/4414.bugfix.md @@ -0,0 +1 @@ +An array `config` with a key that is not a runtime configuration option (for example a misspelled `write_empty_chunk`) now raises a `TypeError` naming the unknown and valid keys. Previously `create_array`, `create`, `open_array` and other functions silently ignored such keys. diff --git a/src/zarr/core/array_spec.py b/src/zarr/core/array_spec.py index 1f4ffd6f09..61667275f9 100644 --- a/src/zarr/core/array_spec.py +++ b/src/zarr/core/array_spec.py @@ -92,8 +92,15 @@ def from_dict(cls, data: ArrayConfigParams) -> Self: """ Create an ArrayConfig from a dict. The keys of that dict are a subset of the attributes of the ArrayConfig class. Any keys missing from that dict will be set to the - the values in the ``array`` namespace of ``zarr.config``. + the values in the ``array`` namespace of ``zarr.config``. Keys that are not + attributes of the ArrayConfig class raise a `TypeError`. """ + unknown = set(data) - {f.name for f in fields(ArrayConfig)} + if unknown: + raise TypeError( + f"Unknown array config keys: {sorted(unknown)}. " + f"Valid keys are {[f.name for f in fields(ArrayConfig)]}." + ) kwargs_out: ArrayConfigParams = {} for f in fields(ArrayConfig): field_name = cast( diff --git a/tests/test_array_spec.py b/tests/test_array_spec.py index 4fbc0b1205..ae94ec4d3d 100644 --- a/tests/test_array_spec.py +++ b/tests/test_array_spec.py @@ -5,7 +5,8 @@ import numpy as np import pytest -from zarr.core.array_spec import ArrayConfig, ArraySpec +import zarr +from zarr.core.array_spec import ArrayConfig, ArrayConfigParams, ArraySpec from zarr.core.buffer import BufferPrototype, default_buffer_prototype from zarr.core.buffer.cpu import NDBuffer from zarr.core.dtype import get_data_type_from_native_dtype @@ -177,3 +178,46 @@ def test_signed_zero_fills_are_distinct(native_dtype: Any, neg_fill: Any, pos_fi def test_unequal_with_invalid_type(obj: Any) -> None: assert (_make_spec() == obj) is False assert _make_spec() != obj + + +@pytest.mark.parametrize( + "data", + [ + {}, + {"order": "F"}, + {"write_empty_chunks": True, "read_missing_chunks": False}, + { + "order": "F", + "write_empty_chunks": True, + "read_missing_chunks": False, + "sharding_coalesce_max_gap_bytes": 1, + "sharding_coalesce_max_bytes": 2, + }, + ], +) +def test_array_config_from_dict(data: ArrayConfigParams) -> None: + """ + ArrayConfig.from_dict takes the given keys from `data` and the rest from the global + configuration. + """ + observed = ArrayConfig.from_dict(data).to_dict() + for key, value in observed.items(): + assert value == data.get(key, zarr.config.get(f"array.{key}")) + + +@pytest.mark.parametrize( + "build", + [ + lambda config: ArrayConfig.from_dict(config), + lambda config: zarr.create_array({}, shape=(4,), dtype="uint8", config=config), + lambda config: zarr.create(shape=(4,), dtype="uint8", config=config), + ], + ids=["from_dict", "create_array", "create"], +) +def test_array_config_unknown_key(build: Callable[[Any], object]) -> None: + """ + An array config with a key that is not an ArrayConfig attribute raises TypeError instead of + being ignored. + """ + with pytest.raises(TypeError, match=r"Unknown array config keys: \['nope'\]"): + build({"order": "C", "nope": 1})