Skip to content

Commit c79f736

Browse files
authored
fix(group): apply config when require_array returns an existing array (#4413)
* fix(group): apply config when require_array returns an existing array require_array forwarded **kwargs, including config, only to create_array in the branch that creates a missing array. Requiring an existing array dropped config and returned it with the global defaults. Apply it with with_config(), as open_array does since #4408. Assisted-by: ClaudeCode:claude-opus-5-5 * docs(changes): add fragment for #4413 Assisted-by: ClaudeCode:claude-opus-5-5 * refactor(group): make config a named parameter of require_array config was read from **kwargs, which is how the existing-array branch came to drop it. It is now a named parameter of Group.require_array and AsyncGroup.require_array, passed explicitly to create_array for a new array and applied with with_config() to an existing one. The existing array comes from getitem, which serves member lookup in general (including consolidated metadata) and takes no runtime config. Assisted-by: ClaudeCode:claude-opus-5-5 * fix(group): parse config the same way on both require_array branches The existing-array branch handed config to with_config, which merged it over the array's current config and rejected an unknown key with the dataclass's "unexpected keyword argument" message. The create branch went through parse_array_config, which fills missing keys from the global configuration and says "Unknown array config keys". The same bad input now raises the same error whichever branch runs, and the docstring's "taken from the global configuration" holds by construction. The error tests run on both branches, and the combinations test passes config=None explicitly instead of omitting the keyword. Assisted-by: ClaudeCode:claude-fable-5-1
1 parent 9e631c6 commit c79f736

3 files changed

Lines changed: 98 additions & 3 deletions

File tree

‎changes/4413.bugfix.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`Group.require_array` now applies the `config` argument when it returns an existing array. Previously `config` was only used when `require_array` created a new array, so an existing array was returned with the global defaults. An invalid `config` now raises instead of being ignored.

‎src/zarr/core/group.py‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
_parse_deprecated_compressor,
2929
create_array,
3030
)
31+
from zarr.core.array_spec import parse_array_config
3132
from zarr.core.attributes import Attributes
3233
from zarr.core.buffer import default_buffer_prototype
3334
from zarr.core.common import (
@@ -1240,6 +1241,7 @@ async def require_array(
12401241
shape: ShapeLike,
12411242
dtype: ZDTypeLike | None = None,
12421243
exact: bool = False,
1244+
config: ArrayConfigLike | None = None,
12431245
**kwargs: Any,
12441246
) -> AnyAsyncArray:
12451247
"""Obtain an array, creating if it doesn't exist.
@@ -1258,6 +1260,9 @@ async def require_array(
12581260
exact : bool, optional
12591261
If True, require `dtype` to match exactly. If false, require
12601262
`dtype` can be cast from array dtype.
1263+
config : ArrayConfigLike or None, default=None
1264+
Runtime configuration for the array, whether it is created or already exists.
1265+
Keys not specified are taken from the global configuration.
12611266
12621267
Returns
12631268
-------
@@ -1284,8 +1289,13 @@ async def require_array(
12841289
if not np.can_cast(ds.dtype, dtype):
12851290
raise TypeError(f"Incompatible dtype ({ds.dtype} vs {dtype})")
12861291
except KeyError:
1287-
ds = await self.create_array(name, shape=shape, dtype=dtype, **kwargs)
1292+
return await self.create_array(name, shape=shape, dtype=dtype, config=config, **kwargs)
12881293

1294+
# `config` is the runtime configuration of the returned array, not stored metadata,
1295+
# so it applies to an existing array too. It is parsed the same way as on the create
1296+
# branch: missing keys come from the global configuration, unknown keys raise.
1297+
if config is not None:
1298+
return ds.with_config(parse_array_config(config))
12891299
return ds
12901300

12911301
async def update_attributes(self, new_attributes: dict[str, Any]) -> AsyncGroup:
@@ -2837,7 +2847,14 @@ def create_array(
28372847
)
28382848
)
28392849

2840-
def require_array(self, name: str, *, shape: ShapeLike, **kwargs: Any) -> AnyArray:
2850+
def require_array(
2851+
self,
2852+
name: str,
2853+
*,
2854+
shape: ShapeLike,
2855+
config: ArrayConfigLike | None = None,
2856+
**kwargs: Any,
2857+
) -> AnyArray:
28412858
"""Obtain an array, creating if it doesn't exist.
28422859
28432860
Other `kwargs` are as per [zarr.Group.create_array][].
@@ -2846,14 +2863,19 @@ def require_array(self, name: str, *, shape: ShapeLike, **kwargs: Any) -> AnyArr
28462863
----------
28472864
name : str
28482865
Array name.
2866+
config : ArrayConfigLike or None, default=None
2867+
Runtime configuration for the array, whether it is created or already exists.
2868+
Keys not specified are taken from the global configuration.
28492869
**kwargs :
28502870
See [zarr.Group.create_array][].
28512871
28522872
Returns
28532873
-------
28542874
a : Array
28552875
"""
2856-
return Array(self._sync(self._async_group.require_array(name, shape=shape, **kwargs)))
2876+
return Array(
2877+
self._sync(self._async_group.require_array(name, shape=shape, config=config, **kwargs))
2878+
)
28572879

28582880
def empty(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> AnyArray:
28592881
"""Create an empty array with the specified shape in this Group. The contents will be filled with

‎tests/test_group.py‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from zarr.abc.store import Store
2323
from zarr.core import sync_group
2424
from zarr.core._info import GroupInfo
25+
from zarr.core.array_spec import ArrayConfig
2526
from zarr.core.buffer import default_buffer_prototype
2627
from zarr.core.config import config as zarr_config
2728
from zarr.core.dtype import Float64, Int32
@@ -62,6 +63,7 @@
6263
from collections.abc import Callable
6364

6465
from zarr.core.array import ShardsLike
66+
from zarr.core.array_spec import ArrayConfigLike
6567
from zarr.core.buffer.core import Buffer
6668
from zarr.core.common import JSON, ChunksLike, ZarrFormat
6769
from zarr.core.dtype import ZDType, ZDTypeLike
@@ -1467,6 +1469,76 @@ async def test_require_array(store: Store, zarr_format: ZarrFormat) -> None:
14671469
await root.require_array("bar", shape=(10,), dtype="int8")
14681470

14691471

1472+
@pytest.mark.parametrize("zarr_format", [2, 3])
1473+
@pytest.mark.parametrize("api", ["sync", "async"])
1474+
@pytest.mark.parametrize("exists", [True, False])
1475+
@pytest.mark.parametrize(
1476+
("config", "expected"),
1477+
[
1478+
(None, ArrayConfig.from_dict({})),
1479+
(
1480+
{"read_missing_chunks": False},
1481+
ArrayConfig.from_dict({"read_missing_chunks": False}),
1482+
),
1483+
(
1484+
{"order": "F", "write_empty_chunks": True},
1485+
ArrayConfig.from_dict({"order": "F", "write_empty_chunks": True}),
1486+
),
1487+
(
1488+
ArrayConfig.from_dict({"read_missing_chunks": False}),
1489+
ArrayConfig.from_dict({"read_missing_chunks": False}),
1490+
),
1491+
],
1492+
)
1493+
async def test_require_array_config(
1494+
zarr_format: ZarrFormat,
1495+
api: Literal["sync", "async"],
1496+
exists: bool,
1497+
config: ArrayConfigLike | None,
1498+
expected: ArrayConfig,
1499+
) -> None:
1500+
"""
1501+
require_array applies the config argument whether it creates the array or returns an
1502+
existing one, with keys missing from a partial config taken from the global defaults.
1503+
"""
1504+
group = Group.from_store(MemoryStore(), zarr_format=zarr_format)
1505+
if exists:
1506+
group.create_array("a", shape=(4,), dtype="uint8")
1507+
if api == "sync":
1508+
observed = group.require_array("a", shape=(4,), dtype="uint8", config=config).config
1509+
else:
1510+
observed = (
1511+
await group._async_group.require_array("a", shape=(4,), dtype="uint8", config=config)
1512+
).config
1513+
assert observed == expected
1514+
1515+
1516+
@pytest.mark.parametrize("exists", [True, False])
1517+
def test_require_array_unknown_config_key(exists: bool) -> None:
1518+
"""
1519+
Requiring an array with a config containing an unknown key raises TypeError, whether the
1520+
array exists or is created.
1521+
"""
1522+
group = Group.from_store(MemoryStore())
1523+
if exists:
1524+
group.create_array("a", shape=(4,), dtype="uint8")
1525+
with pytest.raises(TypeError, match=r"Unknown array config keys: \['nope'\]"):
1526+
group.require_array("a", shape=(4,), dtype="uint8", config={"nope": 1})
1527+
1528+
1529+
@pytest.mark.parametrize("exists", [True, False])
1530+
def test_require_array_invalid_config_value(exists: bool) -> None:
1531+
"""
1532+
Requiring an array with a config containing an invalid value raises ValueError, whether the
1533+
array exists or is created.
1534+
"""
1535+
group = Group.from_store(MemoryStore())
1536+
if exists:
1537+
group.create_array("a", shape=(4,), dtype="uint8")
1538+
with pytest.raises(ValueError, match="Expected instance of bool"):
1539+
group.require_array("a", shape=(4,), dtype="uint8", config={"read_missing_chunks": "yes"})
1540+
1541+
14701542
@pytest.mark.parametrize(
14711543
("dtype", "expected"),
14721544
[

0 commit comments

Comments
 (0)