Skip to content

Commit b67ba62

Browse files
committed
Merge branch 'main' into fix-zarr-load
2 parents e050f7a + 229a6c7 commit b67ba62

9 files changed

Lines changed: 110 additions & 18 deletions

File tree

changes/3127.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
When `zarr.save` has an argument `path=some/path/` and multiple arrays in `args`, the path resulted in `some/path/some/path` due to using the `path`
2+
argument twice while building the array path. This is now fixed.

docs/conf.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ def skip_submodules(
106106
"installation": "user-guide/installation.html",
107107
"api": "api/zarr/index",
108108
"release": "release-notes.html",
109-
"release-notes": "release-notes.html"
110109
}
111110

112111
# The language for content autogenerated by Sphinx. Refer to documentation

docs/user-guide/consolidated_metadata.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,23 @@ removed, or modified, consolidated metadata may not be desirable.
114114
metadata.
115115

116116
.. _Consolidated Metadata: https://github.com/zarr-developers/zarr-specs/pull/309
117+
118+
Stores Without Support for Consolidated Metadata
119+
------------------------------------------------
120+
121+
Some stores may want to opt out of the consolidated metadata mechanism. This
122+
may be for several reasons like:
123+
124+
* They want to maintain read-write consistency, which is challenging with
125+
consolidated metadata.
126+
* They have their own consolidated metadata mechanism.
127+
* They offer good enough performance without need for consolidation.
128+
129+
This type of store can declare it doesn't want consolidation by implementing
130+
`Store.supports_consolidated_metadata` and returning `False`. For stores that don't support
131+
consolidation, Zarr will:
132+
133+
* Raise an error on `consolidate_metadata` calls, maintaining the store in
134+
its unconsolidated state.
135+
* Raise an error in `AsyncGroup.open(..., use_consolidated=True)`
136+
* Not use consolidated metadata in `AsyncGroup.open(..., use_consolidated=None)`

src/zarr/abc/store.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,18 @@ async def _set_many(self, values: Iterable[tuple[str, Buffer]]) -> None:
264264
"""
265265
await gather(*starmap(self.set, values))
266266

267+
@property
268+
def supports_consolidated_metadata(self) -> bool:
269+
"""
270+
Does the store support consolidated metadata?.
271+
272+
If it doesn't an error will be raised on requests to consolidate the metadata.
273+
Returning `False` can be useful for stores which implement their own
274+
consolidation mechanism outside of the zarr-python implementation.
275+
"""
276+
277+
return True
278+
267279
@property
268280
@abstractmethod
269281
def supports_deletes(self) -> bool:

src/zarr/api/asynchronous.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,8 @@ async def consolidate_metadata(
176176
Consolidate the metadata of all nodes in a hierarchy.
177177
178178
Upon completion, the metadata of the root node in the Zarr hierarchy will be
179-
updated to include all the metadata of child nodes.
179+
updated to include all the metadata of child nodes. For Stores that do
180+
not support consolidated metadata, this operation raises a ``TypeError``.
180181
181182
Parameters
182183
----------
@@ -196,10 +197,18 @@ async def consolidate_metadata(
196197
-------
197198
group: AsyncGroup
198199
The group, with the ``consolidated_metadata`` field set to include
199-
the metadata of each child node.
200+
the metadata of each child node. If the Store doesn't support
201+
consolidated metadata, this function raises a `TypeError`.
202+
See ``Store.supports_consolidated_metadata``.
200203
"""
201204
store_path = await make_store_path(store, path=path)
202205

206+
if not store_path.store.supports_consolidated_metadata:
207+
store_name = type(store_path.store).__name__
208+
raise TypeError(
209+
f"The Zarr Store in use ({store_name}) doesn't support consolidated metadata",
210+
)
211+
203212
group = await AsyncGroup.open(store_path, zarr_format=zarr_format, use_consolidated=False)
204213
group.store_path.store._check_writable()
205214

@@ -512,13 +521,12 @@ async def save_group(
512521
raise ValueError("at least one array must be provided")
513522
aws = []
514523
for i, arr in enumerate(args):
515-
_path = f"{path}/arr_{i}" if path is not None else f"arr_{i}"
516524
aws.append(
517525
save_array(
518526
store_path,
519527
arr,
520528
zarr_format=zarr_format,
521-
path=_path,
529+
path=f"arr_{i}",
522530
storage_options=storage_options,
523531
)
524532
)

src/zarr/api/synchronous.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ def consolidate_metadata(
8181
Consolidate the metadata of all nodes in a hierarchy.
8282
8383
Upon completion, the metadata of the root node in the Zarr hierarchy will be
84-
updated to include all the metadata of child nodes.
84+
updated to include all the metadata of child nodes. For Stores that do
85+
not use consolidated metadata, this operation raises a `TypeError`.
8586
8687
Parameters
8788
----------
@@ -101,7 +102,10 @@ def consolidate_metadata(
101102
-------
102103
group: Group
103104
The group, with the ``consolidated_metadata`` field set to include
104-
the metadata of each child node.
105+
the metadata of each child node. If the Store doesn't support
106+
consolidated metadata, this function raises a `TypeError`.
107+
See ``Store.supports_consolidated_metadata``.
108+
105109
"""
106110
return Group(sync(async_api.consolidate_metadata(store, path=path, zarr_format=zarr_format)))
107111

src/zarr/core/group.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -490,10 +490,11 @@ async def open(
490490
491491
By default, consolidated metadata is used if it's present in the
492492
store (in the ``zarr.json`` for Zarr format 3 and in the ``.zmetadata`` file
493-
for Zarr format 2).
493+
for Zarr format 2) and the Store supports it.
494494
495-
To explicitly require consolidated metadata, set ``use_consolidated=True``,
496-
which will raise an exception if consolidated metadata is not found.
495+
To explicitly require consolidated metadata, set ``use_consolidated=True``.
496+
In this case, if the Store doesn't support consolidation or consolidated metadata is
497+
not found, a ``ValueError`` exception is raised.
497498
498499
To explicitly *not* use consolidated metadata, set ``use_consolidated=False``,
499500
which will fall back to using the regular, non consolidated metadata.
@@ -503,6 +504,16 @@ async def open(
503504
to load consolidated metadata from a non-default key.
504505
"""
505506
store_path = await make_store_path(store)
507+
if not store_path.store.supports_consolidated_metadata:
508+
# Fail if consolidated metadata was requested but the Store doesn't support it
509+
if use_consolidated:
510+
store_name = type(store_path.store).__name__
511+
raise ValueError(
512+
f"The Zarr store in use ({store_name}) doesn't support consolidated metadata."
513+
)
514+
515+
# if use_consolidated was None (optional), the Store dictates it doesn't want consolidation
516+
use_consolidated = False
506517

507518
consolidated_key = ZMETADATA_V2_JSON
508519

tests/test_api.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -229,22 +229,23 @@ async def test_open_group_unspecified_version(
229229
@pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"])
230230
@pytest.mark.parametrize("n_args", [10, 1, 0])
231231
@pytest.mark.parametrize("n_kwargs", [10, 1, 0])
232-
def test_save(store: Store, n_args: int, n_kwargs: int) -> None:
232+
@pytest.mark.parametrize("path", [None, "some_path"])
233+
def test_save(store: Store, n_args: int, n_kwargs: int, path: None | str) -> None:
233234
data = np.arange(10)
234235
args = [np.arange(10) for _ in range(n_args)]
235236
kwargs = {f"arg_{i}": data for i in range(n_kwargs)}
236237

237238
if n_kwargs == 0 and n_args == 0:
238239
with pytest.raises(ValueError):
239-
save(store)
240+
save(store, path=path)
240241
elif n_args == 1 and n_kwargs == 0:
241-
save(store, *args)
242-
array = zarr.api.synchronous.open(store)
242+
save(store, *args, path=path)
243+
array = zarr.api.synchronous.open(store, path=path)
243244
assert isinstance(array, Array)
244245
assert_array_equal(array[:], data)
245246
else:
246-
save(store, *args, **kwargs) # type: ignore [arg-type]
247-
group = zarr.api.synchronous.open(store)
247+
save(store, *args, path=path, **kwargs) # type: ignore [arg-type]
248+
group = zarr.api.synchronous.open(store, path=path)
248249
assert isinstance(group, Group)
249250
for array in group.array_values():
250251
assert_array_equal(array[:], data)
@@ -384,8 +385,8 @@ def test_array_order_warns(order: MemoryOrder | None, zarr_format: ZarrFormat) -
384385
# assert "LazyLoader: " in repr(loader)
385386

386387

387-
def test_load_array(memory_store: Store) -> None:
388-
store = memory_store
388+
def test_load_array(sync_store: Store) -> None:
389+
store = sync_store
389390
foo = np.arange(100)
390391
bar = np.arange(100, 0, -1)
391392
save(store, foo=foo, bar=bar)

tests/test_metadata/test_consolidated.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,3 +651,38 @@ async def test_consolidated_metadata_encodes_special_chars(
651651
elif zarr_format == 3:
652652
assert root_metadata["child"]["attributes"]["test"] == expected_fill_value
653653
assert root_metadata["time"]["fill_value"] == expected_fill_value
654+
655+
656+
class NonConsolidatedStore(zarr.storage.MemoryStore):
657+
"""A store that doesn't support consolidated metadata"""
658+
659+
@property
660+
def supports_consolidated_metadata(self) -> bool:
661+
return False
662+
663+
664+
async def test_consolidate_metadata_raises_for_self_consolidating_stores():
665+
"""Verify calling consolidate_metadata on a non supporting stores raises an error."""
666+
667+
memory_store = NonConsolidatedStore()
668+
root = await zarr.api.asynchronous.create_group(store=memory_store)
669+
await root.create_group("a/b")
670+
671+
with pytest.raises(TypeError, match="doesn't support consolidated metadata"):
672+
await zarr.api.asynchronous.consolidate_metadata(memory_store)
673+
674+
675+
async def test_open_group_in_non_consolidating_stores():
676+
memory_store = NonConsolidatedStore()
677+
root = await zarr.api.asynchronous.create_group(store=memory_store)
678+
await root.create_group("a/b")
679+
680+
# Opening a group without consolidatedion works as expected
681+
await AsyncGroup.open(memory_store, use_consolidated=False)
682+
683+
# let the Store opt out of consolidation
684+
await AsyncGroup.open(memory_store, use_consolidated=None)
685+
686+
# Opening a group with use_consolidated=True should fail
687+
with pytest.raises(ValueError, match="doesn't support consolidated metadata"):
688+
await AsyncGroup.open(memory_store, use_consolidated=True)

0 commit comments

Comments
 (0)