Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
### Fixed
- Added missing validation for dataset reference target types to ensure correct `RefSpec.target_type` matching. @sejalpunwatkar [#1429](https://github.com/hdmf-dev/hdmf/pull/1429)
- Fixed reading a `DynamicTable` that contains a named link to a `VectorData` (e.g., `MeaningsTable.target`). The link target was being picked up as an extra column, causing a `"Columns must be the same length"` error when the target column's row count differed from the table's own row count. @rly [#1445](https://github.com/hdmf-dev/hdmf/pull/1445)
- Fixed building datasets with `dtype: isodatetime` when the spec uses `data_type_inc` (e.g., `VectorData`-extending columns). `datetime`/`date` values are now converted to ISO strings before dtype resolution instead of failing with `"Expected unicode or ascii string"`. @rly [#1314](https://github.com/hdmf-dev/hdmf/pull/1314)


## HDMF 5.1.0 (March 24, 2026)
Expand Down
11 changes: 8 additions & 3 deletions src/hdmf/build/objectmapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,7 +627,11 @@ def get_attr_value(self, **kwargs):
if isinstance(attr_val, TermSetWrapper):
attr_val = attr_val.value
if attr_val is not None:
attr_val = self.__convert_string(attr_val, spec)
if not isinstance(attr_val, Data):
# Data containers are unwrapped and string-converted later in build(),
# after the Data sub-builder is created. Converting here would operate
# on the Data wrapper itself and produce the wrong result.
attr_val = self.__convert_string(attr_val, spec)
spec_dt = self.__get_data_type(spec)
if spec_dt is not None:
try:
Expand Down Expand Up @@ -660,6 +664,7 @@ def __apply_string_type(value, string_type):
# NOTE: if a user passes a h5py.Dataset that is not wrapped with a hdmf.utils.StrDataset,
# then this conversion may not be correct. Users should unpack their string h5py.Datasets
# into a numpy array (or wrap them in StrDataset) before passing them to a container object.
# NOTE: this will convert datasets and arrays to lists of lists.
if hasattr(value, '__iter__') and not isinstance(value, (str, bytes)):
return [__apply_string_type(item, string_type) for item in value]
else:
Expand All @@ -673,8 +678,7 @@ def __apply_string_type(value, string_type):
else:
ret = str(value)
elif isinstance(spec, DatasetSpec):
# TODO: make sure we can handle specs with data_type_inc set
if spec.data_type_inc is None and spec.dtype is not None:
if spec.dtype is not None:
string_type = None
if 'text' in spec.dtype:
string_type = str
Expand Down Expand Up @@ -849,6 +853,7 @@ def build(self, **kwargs):
data = container.data.value
else:
data = container.data
data = self.__convert_string(data, spec)
bldr_data, dtype = self.convert_dtype(spec, data, spec_dtype=spec_dtype)
except Exception as ex:
msg = f"could not resolve dtype for {type(container).__name__} '{container.name}'"
Expand Down
86 changes: 85 additions & 1 deletion tests/unit/build_tests/mapper_tests/test_build_datetime.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from hdmf.utils import docval, getargs
from hdmf import Container
from hdmf import Container, Data
from hdmf.spec import GroupSpec, DatasetSpec
from hdmf.testing import TestCase
from datetime import datetime, date
Expand All @@ -25,6 +25,38 @@ def data(self):
return self.__data


class Column(Data):

@docval({'name': 'name', 'type': str, 'doc': 'the name of this Column'},
{'name': 'data', 'type': ('data', 'array_data'), 'doc': 'some data'})
def __init__(self, **kwargs):
name, data = getargs('name', 'data', kwargs)
super().__init__(name=name, data=data)

@property
def data_type(self):
return 'Column'


class BarWithColumnData(Container):

@docval({'name': 'name', 'type': str, 'doc': 'the name of this Bar'},
{'name': 'data', 'type': Column, 'doc': 'some data'})
def __init__(self, **kwargs):
name, data = getargs('name', 'data', kwargs)
super().__init__(name=name)
self.__data = data
data.parent = self

@property
def data_type(self):
return 'Bar'

@property
def data(self):
return self.__data


class TestBuildDatasetDateTime(TestCase):
"""Test that building a dataset with dtype isodatetime works with datetime and date objects."""

Expand Down Expand Up @@ -83,3 +115,55 @@ def test_date_array(self):
ret = builder.get('data')
assert ret.data == [b'2023-07-09', b'2023-07-10']
assert ret.dtype == 'ascii'


def test_datetime_array_ext_spec(self):
column_spec = DatasetSpec(data_type_def='Column', doc='an example dataset', dims=(None,))
bar_spec = GroupSpec(
doc='A test group specification with a data type',
data_type_def='Bar',
datasets=[
DatasetSpec(
data_type_inc='Column',
doc='an example dataset',
name='data',
dtype='isodatetime',
),
],
)
type_map = create_test_type_map([bar_spec, column_spec], {'Bar': BarWithColumnData, 'Column': Column})

bar_inst = BarWithColumnData(
name='my_bar',
data=Column(name='data', data=[datetime(2023, 7, 9), datetime(2023, 7, 10)]),
)
builder = type_map.build(bar_inst)
ret = builder.get('data')
assert ret.data == [b'2023-07-09T00:00:00', b'2023-07-10T00:00:00']
assert ret.dtype == 'ascii'

def test_date_array_ext_spec(self):
# This mirrors the traceback in issue #1311, which used datetime.date on a VectorData extension.
column_spec = DatasetSpec(data_type_def='Column', doc='an example dataset', dims=(None,))
bar_spec = GroupSpec(
doc='A test group specification with a data type',
data_type_def='Bar',
datasets=[
DatasetSpec(
data_type_inc='Column',
doc='an example dataset',
name='data',
dtype='isodatetime',
),
],
)
type_map = create_test_type_map([bar_spec, column_spec], {'Bar': BarWithColumnData, 'Column': Column})

bar_inst = BarWithColumnData(
name='my_bar',
data=Column(name='data', data=[date(2023, 7, 9), date(2023, 7, 10)]),
)
builder = type_map.build(bar_inst)
ret = builder.get('data')
assert ret.data == [b'2023-07-09', b'2023-07-10']
assert ret.dtype == 'ascii'
Loading