Skip to content

Commit 998a7b4

Browse files
authored
Add support for custom metadata. Tested :) (#19)
Add support for custom metadata. Tested :)
1 parent fbea400 commit 998a7b4

File tree

3 files changed

+54
-10
lines changed

3 files changed

+54
-10
lines changed

funlib/persistence/arrays/array.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import logging
22
from functools import reduce
3-
from typing import Optional, Sequence, Union
3+
from typing import Optional, Sequence, Union, Any
44

55
import dask.array as da
66
import numpy as np
77
from dask.array.optimization import fuse_slice
8+
from zarr import Array as ZarrArray
89

910
from funlib.geometry import Coordinate, Roi
1011

@@ -83,6 +84,10 @@ def __init__(
8384
shape=self._source_data.shape,
8485
)
8586

87+
# used for custom metadata unrelated to indexing with physical units
88+
# only used if not reading from zarr and there is no built in `.attrs`
89+
self._attrs: dict[str, Any] = {}
90+
8691
if lazy_op is not None:
8792
self.apply_lazy_ops(lazy_op)
8893

@@ -93,6 +98,18 @@ def __init__(
9398

9499
self.validate()
95100

101+
@property
102+
def attrs(self) -> dict:
103+
"""
104+
Return dict that can be used to store custom metadata. Will be persistent
105+
for zarr arrays. If reading from zarr, any existing metadata (such as
106+
voxel_size, axis_names, etc.) will also be exposed here.
107+
"""
108+
if isinstance(self._source_data, ZarrArray):
109+
return self._source_data.attrs
110+
else:
111+
return self._attrs
112+
96113
@property
97114
def chunk_shape(self) -> Coordinate:
98115
return Coordinate(self.data.chunksize)

funlib/persistence/arrays/datasets.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import logging
2-
from typing import Optional, Sequence, Union
2+
from typing import Any, Optional, Sequence, Union
33

44
import numpy as np
55
import zarr
@@ -128,6 +128,7 @@ def prepare_ds(
128128
chunk_shape: Optional[Sequence[int]] = None,
129129
dtype: DTypeLike = np.float32,
130130
mode: str = "a",
131+
custom_metadata: dict[str, Any] | None = None,
131132
**kwargs,
132133
) -> Array:
133134
"""Prepare a Zarr or N5 dataset.
@@ -179,6 +180,11 @@ def prepare_ds(
179180
The mode to open the dataset in.
180181
See https://zarr.readthedocs.io/en/stable/api/creation.html#zarr.creation.open_array
181182
183+
custom_metadata:
184+
185+
A dictionary of custom metadata to add to the dataset. This will be written to the
186+
zarr .attrs object.
187+
182188
kwargs:
183189
184190
See additional arguments available here:
@@ -319,14 +325,18 @@ def prepare_ds(
319325
raise ArrayNotFoundError(f"Nothing found at path {store}")
320326

321327
default_metadata_format = get_default_metadata_format()
322-
ds.attrs.put(
323-
{
324-
default_metadata_format.axis_names_attr: combined_metadata.axis_names,
325-
default_metadata_format.units_attr: combined_metadata.units,
326-
default_metadata_format.voxel_size_attr: combined_metadata.voxel_size,
327-
default_metadata_format.offset_attr: combined_metadata.offset,
328-
}
329-
)
328+
our_metadata = {
329+
default_metadata_format.axis_names_attr: combined_metadata.axis_names,
330+
default_metadata_format.units_attr: combined_metadata.units,
331+
default_metadata_format.voxel_size_attr: combined_metadata.voxel_size,
332+
default_metadata_format.offset_attr: combined_metadata.offset,
333+
}
334+
# check keys don't conflict
335+
if custom_metadata is not None:
336+
assert set(our_metadata.keys()).isdisjoint(custom_metadata.keys())
337+
our_metadata.update(custom_metadata)
338+
339+
ds.attrs.put(our_metadata)
330340

331341
# open array
332342
array = Array(ds, offset, voxel_size, axis_names, units)

tests/test_datasets.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,23 @@
1515
}
1616

1717

18+
@pytest.mark.parametrize("store", stores.keys())
19+
def test_metadata(tmpdir, store):
20+
store = tmpdir / store
21+
22+
# test prepare_ds creates array if it does not exist and mode is write
23+
array = prepare_ds(
24+
store,
25+
(10, 10),
26+
mode="w",
27+
custom_metadata={"custom": "metadata"},
28+
)
29+
assert array.attrs["custom"] == "metadata"
30+
array.attrs["custom2"] = "new metadata"
31+
32+
assert open_ds(store).attrs["custom2"] == "new metadata"
33+
34+
1835
@pytest.mark.parametrize("store", stores.keys())
1936
@pytest.mark.parametrize("dtype", [np.float32, np.uint8, np.uint64])
2037
def test_helpers(tmpdir, store, dtype):

0 commit comments

Comments
 (0)