Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrap data in set_data_io with a DataChunkIterator to support overriding hdf5 dataset backend configurations #1172

Merged
merged 19 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# HDMF Changelog

## HDMF 3.14.5 (September 6, 2024)
## HDMF 3.14.5 (September 17, 2024)

### Enhancements
- Added support for overriding backend configurations of `h5py.Dataset` objects in `Container.set_data_io`. @pauladkisson [#1172](https://github.com/hdmf-dev/hdmf/pull/1172)

### Bug fixes
- Fixed bug in writing of string arrays to an HDF5 file that were read from an HDF5 file that was introduced in 3.14.4. @rly @stephprince
Expand Down
48 changes: 43 additions & 5 deletions src/hdmf/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from abc import abstractmethod
from collections import OrderedDict
from copy import deepcopy
from typing import Type
from typing import Type, Optional
from uuid import uuid4
from warnings import warn
import os
Expand All @@ -11,7 +11,7 @@
import numpy as np
import pandas as pd

from .data_utils import DataIO, append_data, extend_data
from .data_utils import DataIO, append_data, extend_data, AbstractDataChunkIterator
from .utils import docval, get_docval, getargs, ExtenderMeta, get_data_shape, popargs, LabelledDict

from .term_set import TermSet, TermSetWrapper
Expand Down Expand Up @@ -826,7 +826,14 @@ def __smart_str_dict(d, num_indent):
out += '\n' + indent + right_br
return out

def set_data_io(self, dataset_name: str, data_io_class: Type[DataIO], data_io_kwargs: dict = None, **kwargs):
def set_data_io(
self,
dataset_name: str,
data_io_class: Type[DataIO],
data_io_kwargs: dict = None,
data_chunk_iterator_class: Optional[Type[AbstractDataChunkIterator]] = None,
data_chunk_iterator_kwargs: dict = None, **kwargs
):
"""
Apply DataIO object to a dataset field of the Container.

pauladkisson marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -838,9 +845,18 @@ def set_data_io(self, dataset_name: str, data_io_class: Type[DataIO], data_io_kw
Class to use for DataIO, e.g. H5DataIO or ZarrDataIO
data_io_kwargs: dict
keyword arguments passed to the constructor of the DataIO class.
data_chunk_iterator_class: Type[AbstractDataChunkIterator]
Class to use for DataChunkIterator. If None, no DataChunkIterator is used.
data_chunk_iterator_kwargs: dict
keyword arguments passed to the constructor of the DataChunkIterator class.
**kwargs:
DEPRECATED. Use data_io_kwargs instead.
kwargs are passed to the constructor of the DataIO class.

Notes
-----
If data_chunk_iterator_class is not None, the data is wrapped in the DataChunkIterator before being wrapped in
the DataIO. This allows for rewriting the backend configuration of hdf5 datasets.
"""
if kwargs or (data_io_kwargs is None):
warn(
Expand All @@ -851,8 +867,11 @@ def set_data_io(self, dataset_name: str, data_io_class: Type[DataIO], data_io_kw
)
data_io_kwargs = kwargs
data = self.fields.get(dataset_name)
data_chunk_iterator_kwargs = data_chunk_iterator_kwargs or dict()
if data is None:
raise ValueError(f"{dataset_name} is None and cannot be wrapped in a DataIO class")
if data_chunk_iterator_class is not None:
data = data_chunk_iterator_class(data=data, **data_chunk_iterator_kwargs)
self.fields[dataset_name] = data_io_class(data=data, **data_io_kwargs)


Expand Down Expand Up @@ -896,7 +915,13 @@ def set_dataio(self, **kwargs):
dataio.data = self.__data
self.__data = dataio

def set_data_io(self, data_io_class: Type[DataIO], data_io_kwargs: dict) -> None:
def set_data_io(
self,
data_io_class: Type[DataIO],
data_io_kwargs: dict,
data_chunk_iterator_class: Optional[Type[AbstractDataChunkIterator]] = None,
data_chunk_iterator_kwargs: dict = None,
) -> None:
"""
Apply DataIO object to the data held by this Data object.

Expand All @@ -906,8 +931,21 @@ def set_data_io(self, data_io_class: Type[DataIO], data_io_kwargs: dict) -> None
The DataIO to apply to the data held by this Data.
data_io_kwargs: dict
The keyword arguments to pass to the DataIO.
data_chunk_iterator_class: Type[AbstractDataChunkIterator]
The DataChunkIterator to use for the DataIO. If None, no DataChunkIterator is used.
data_chunk_iterator_kwargs: dict
The keyword arguments to pass to the DataChunkIterator.

Notes
-----
If data_chunk_iterator_class is not None, the data is wrapped in the DataChunkIterator before being wrapped in
the DataIO. This allows for rewriting the backend configuration of hdf5 datasets.
"""
self.__data = data_io_class(data=self.__data, **data_io_kwargs)
data_chunk_iterator_kwargs = data_chunk_iterator_kwargs or dict()
data = self.__data
if data_chunk_iterator_class is not None:
data = data_chunk_iterator_class(data=data, **data_chunk_iterator_kwargs)
self.__data = data_io_class(data=data, **data_io_kwargs)

@docval({'name': 'func', 'type': types.FunctionType, 'doc': 'a function to transform *data*'})
def transform(self, **kwargs):
Expand Down
52 changes: 52 additions & 0 deletions tests/unit/test_io_hdf5_h5tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -3801,6 +3801,11 @@ def __init__(self, **kwargs):
self.data2 = kwargs["data2"]

self.obj = ContainerWithData("name", [1, 2, 3, 4, 5], None)
self.file_path = get_temp_filepath()

def tearDown(self):
if os.path.exists(self.file_path):
os.remove(self.file_path)

def test_set_data_io(self):
self.obj.set_data_io("data1", H5DataIO, data_io_kwargs=dict(chunks=True))
Expand All @@ -3823,6 +3828,31 @@ def test_set_data_io_old_api(self):
self.assertIsInstance(self.obj.data1, H5DataIO)
self.assertTrue(self.obj.data1.io_settings["chunks"])

def test_set_data_io_h5py_dataset(self):
file = File(self.file_path, 'w')
data = file.create_dataset('data', data=[1, 2, 3, 4, 5], chunks=(3,))
class ContainerWithData(Container):
__fields__ = ('data',)

@docval(
{"name": "name", "doc": "name", "type": str},
{'name': 'data', 'doc': 'field1 doc', 'type': h5py.Dataset},
)
def __init__(self, **kwargs):
super().__init__(name=kwargs["name"])
self.data = kwargs["data"]

container = ContainerWithData("name", data)
container.set_data_io(
"data",
H5DataIO,
data_io_kwargs=dict(chunks=(2,)),
data_chunk_iterator_class=DataChunkIterator,
)

self.assertIsInstance(container.data, H5DataIO)
self.assertEqual(container.data.io_settings["chunks"], (2,))
file.close()

class TestDataSetDataIO(TestCase):

Expand All @@ -3831,8 +3861,30 @@ class MyData(Data):
pass

self.data = MyData("my_data", [1, 2, 3])
self.file_path = get_temp_filepath()

def tearDown(self):
if os.path.exists(self.file_path):
os.remove(self.file_path)

def test_set_data_io(self):
self.data.set_data_io(H5DataIO, dict(chunks=True))
assert isinstance(self.data.data, H5DataIO)
assert self.data.data.io_settings["chunks"]

def test_set_data_io_h5py_dataset(self):
file = File(self.file_path, 'w')
data = file.create_dataset('data', data=[1, 2, 3, 4, 5], chunks=(3,))
class MyData(Data):
pass

my_data = MyData("my_data", data)
my_data.set_data_io(
H5DataIO,
data_io_kwargs=dict(chunks=(2,)),
data_chunk_iterator_class=DataChunkIterator,
)

self.assertIsInstance(my_data.data, H5DataIO)
self.assertEqual(my_data.data.io_settings["chunks"], (2,))
file.close()
Loading