Skip to content

Commit ad5ed23

Browse files
authored
Merge branch 'dev' into workflow-updates
2 parents 9cd8b2a + 8b42af3 commit ad5ed23

File tree

10 files changed

+200
-13
lines changed

10 files changed

+200
-13
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
# PyNWB Changelog
22

3-
## PyNWB 2.6.1 (Upcoming)
3+
## PyNWB 2.7.0 (Upcoming)
44

55
### Enhancements and minor changes
66
- Added support for python 3.12 and upgraded dependency versions. This also includes infrastructure updates for developers. @mavaylon1 [#1853](https://github.com/NeurodataWithoutBorders/pynwb/pull/1853)
77

8+
### Bug fixes
9+
- Fix bug with reading file with linked `TimeSeriesReferenceVectorData` @rly [#1865](https://github.com/NeurodataWithoutBorders/pynwb/pull/1865)
10+
- Fix bug where extra keyword arguments could not be passed to `NWBFile.add_{x}_column`` for use in custom `VectorData`` classes. @rly [#1861](https://github.com/NeurodataWithoutBorders/pynwb/pull/1861)
11+
812
## PyNWB 2.6.0 (February 21, 2024)
913

1014
### Enhancements and minor changes

requirements-dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# compute coverage, and create test environments. note that depending on the version of python installed, different
33
# versions of requirements may be installed due to package incompatibilities.
44
#
5-
black==23.10.1
5+
black==24.3.0
66
codespell==2.2.6
77
coverage==7.3.2
88
pytest==7.4.3

src/pynwb/file.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@ def __check_epochs(self):
608608
if self.epochs is None:
609609
self.epochs = TimeIntervals(name='epochs', description='experimental epochs')
610610

611-
@docval(*get_docval(TimeIntervals.add_column))
611+
@docval(*get_docval(TimeIntervals.add_column), allow_extra=True)
612612
def add_epoch_column(self, **kwargs):
613613
"""
614614
Add a column to the epoch table.
@@ -645,7 +645,7 @@ def __check_electrodes(self):
645645
if self.electrodes is None:
646646
self.electrodes = ElectrodeTable()
647647

648-
@docval(*get_docval(DynamicTable.add_column))
648+
@docval(*get_docval(DynamicTable.add_column), allow_extra=True)
649649
def add_electrode_column(self, **kwargs):
650650
"""
651651
Add a column to the electrode table.
@@ -747,7 +747,7 @@ def __check_units(self):
747747
if self.units is None:
748748
self.units = Units(name='units', description='Autogenerated by NWBFile')
749749

750-
@docval(*get_docval(Units.add_column))
750+
@docval(*get_docval(Units.add_column), allow_extra=True)
751751
def add_unit_column(self, **kwargs):
752752
"""
753753
Add a column to the unit table.
@@ -770,7 +770,7 @@ def __check_trials(self):
770770
if self.trials is None:
771771
self.trials = TimeIntervals(name='trials', description='experimental trials')
772772

773-
@docval(*get_docval(DynamicTable.add_column))
773+
@docval(*get_docval(DynamicTable.add_column), allow_extra=True)
774774
def add_trial_column(self, **kwargs):
775775
"""
776776
Add a column to the trial table.
@@ -798,7 +798,7 @@ def __check_invalid_times(self):
798798
description='time intervals to be removed from analysis'
799799
)
800800

801-
@docval(*get_docval(DynamicTable.add_column))
801+
@docval(*get_docval(DynamicTable.add_column), allow_extra=True)
802802
def add_invalid_times_column(self, **kwargs):
803803
"""
804804
Add a column to the invalid times table.

src/pynwb/io/epoch.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from hdmf.build import LinkBuilder
12
from hdmf.common.table import VectorData
23
from hdmf.common.io.table import DynamicTableMap
34

@@ -8,13 +9,18 @@
89

910
@register_map(TimeIntervals)
1011
class TimeIntervalsMap(DynamicTableMap):
11-
pass
1212

1313
@DynamicTableMap.constructor_arg('columns')
1414
def columns_carg(self, builder, manager):
1515
# handle the case when a TimeIntervals is read with a non-TimeSeriesReferenceVectorData "timeseries" column
1616
# this is the case for NWB schema v2.4 and earlier, where the timeseries column was a regular VectorData.
1717
timeseries_builder = builder.get('timeseries')
18+
19+
# handle the case when the TimeIntervals has a "timeseries" column that is a link (e.g., it exists in
20+
# a different file that was shallow copied to this file).
21+
if isinstance(timeseries_builder, LinkBuilder):
22+
timeseries_builder = timeseries_builder.builder
23+
1824
# if we have a timeseries column and the type is VectorData instead of TimeSeriesReferenceVectorData
1925
if (timeseries_builder is not None and
2026
timeseries_builder.attributes['neurodata_type'] != 'TimeSeriesReferenceVectorData'):

src/pynwb/testing/testh5io.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from abc import ABCMeta, abstractmethod
55
import warnings
66

7-
from pynwb import NWBFile, NWBHDF5IO, validate as pynwb_validate
7+
from pynwb import NWBFile, NWBHDF5IO, get_manager, validate as pynwb_validate
88
from .utils import remove_test_file
99
from hdmf.backends.warnings import BrokenLinkWarning
1010
from hdmf.build.warnings import MissingRequiredBuildWarning
@@ -247,7 +247,11 @@ def tearDown(self):
247247
remove_test_file(self.filename)
248248
remove_test_file(self.export_filename)
249249

250-
def getContainerType() -> str:
250+
def get_manager(self):
251+
return get_manager() # get the pynwb manager unless overridden
252+
253+
@abstractmethod
254+
def getContainerType(self) -> str:
251255
"""Return the name of the type of Container being tested, for test ID purposes."""
252256
raise NotImplementedError('Cannot run test unless getContainerType is implemented.')
253257

@@ -296,13 +300,13 @@ def roundtripContainer(self, cache_spec=True):
296300

297301
# catch all warnings
298302
with warnings.catch_warnings(record=True) as ws:
299-
with NWBHDF5IO(self.filename, mode='w') as write_io:
303+
with NWBHDF5IO(self.filename, mode='w', manager=self.get_manager()) as write_io:
300304
write_io.write(self.nwbfile, cache_spec=cache_spec)
301305

302306
self.validate()
303307

304308
# this is not closed until tearDown() or an exception from self.getContainer below
305-
self.reader = NWBHDF5IO(self.filename, mode='r')
309+
self.reader = NWBHDF5IO(self.filename, mode='r', manager=self.get_manager())
306310
self.read_nwbfile = self.reader.read()
307311

308312
# parse warnings and raise exceptions for certain types of warnings
@@ -340,7 +344,7 @@ def roundtripExportContainer(self, cache_spec=True):
340344
self.validate()
341345

342346
# this is not closed until tearDown() or an exception from self.getContainer below
343-
self.export_reader = NWBHDF5IO(self.export_filename, mode='r')
347+
self.export_reader = NWBHDF5IO(self.export_filename, mode='r', manager=self.get_manager())
344348
self.read_exported_nwbfile = self.export_reader.read()
345349

346350
# parse warnings and raise exceptions for certain types of warnings
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from hdmf.build import BuildManager
2+
from hdmf.common import VectorData
3+
from hdmf.utils import docval, get_docval, popargs
4+
5+
from pynwb import NWBFile
6+
from pynwb.spec import NWBDatasetSpec, NWBAttributeSpec
7+
from pynwb.testing import NWBH5IOFlexMixin, TestCase
8+
9+
from ..helpers.utils import create_test_extension
10+
11+
12+
class TestDynamicTableCustomColumnWithArgs(NWBH5IOFlexMixin, TestCase):
13+
14+
class SubVectorData(VectorData):
15+
__fields__ = ('extra_kwarg', )
16+
17+
@docval(
18+
*get_docval(VectorData.__init__, "name", "description", "data"),
19+
{'name': 'extra_kwarg', 'type': 'str', 'doc': 'An extra kwarg.'},
20+
)
21+
def __init__(self, **kwargs):
22+
extra_kwarg = popargs('extra_kwarg', kwargs)
23+
super().__init__(**kwargs)
24+
self.extra_kwarg = extra_kwarg
25+
26+
def setUp(self):
27+
"""Set up an extension with a custom VectorData column."""
28+
29+
spec = NWBDatasetSpec(
30+
neurodata_type_def='SubVectorData',
31+
neurodata_type_inc='VectorData',
32+
doc='A custom VectorData column.',
33+
dtype='text',
34+
shape=(None,),
35+
attributes=[
36+
NWBAttributeSpec(
37+
name="extra_kwarg",
38+
doc='An extra kwarg.',
39+
dtype='text'
40+
),
41+
],
42+
)
43+
44+
self.type_map = create_test_extension([spec], {"SubVectorData": self.SubVectorData})
45+
self.manager = BuildManager(self.type_map)
46+
super().setUp()
47+
48+
def get_manager(self):
49+
return self.manager
50+
51+
def getContainerType(self):
52+
return "TrialsWithCustomColumnsWithArgs"
53+
54+
def addContainer(self):
55+
""" Add the test DynamicTable to the given NWBFile """
56+
self.nwbfile.add_trial_column(
57+
name="test",
58+
description="test",
59+
col_cls=self.SubVectorData,
60+
extra_kwarg="test_extra_kwarg"
61+
)
62+
self.nwbfile.add_trial(start_time=1.0, stop_time=2.0, test="test_data")
63+
64+
def getContainer(self, nwbfile: NWBFile):
65+
return nwbfile.trials["test"]
66+
67+
def test_roundtrip(self):
68+
super().test_roundtrip()
69+
assert isinstance(self.read_container, self.SubVectorData)
70+
assert self.read_container.extra_kwarg == "test_extra_kwarg"
71+
72+
def test_roundtrip_export(self):
73+
super().test_roundtrip_export()
74+
assert isinstance(self.read_container, self.SubVectorData)
75+
assert self.read_container.extra_kwarg == "test_extra_kwarg"
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import os
2+
from pynwb.base import TimeSeriesReference
3+
from pynwb import NWBHDF5IO
4+
from pynwb.testing import TestCase
5+
from pynwb.testing.mock.file import mock_NWBFile
6+
from pynwb.testing.mock.base import mock_TimeSeries
7+
8+
9+
class TestFileCopy(TestCase):
10+
11+
def setUp(self):
12+
self.path1 = "test_a.h5"
13+
self.path2 = "test_b.h5"
14+
15+
def tearDown(self):
16+
if os.path.exists(self.path1):
17+
os.remove(self.path1)
18+
if os.path.exists(self.path2):
19+
os.remove(self.path2)
20+
21+
def test_copy_file_link_timeintervals_timeseries(self):
22+
"""Test copying a file with a TimeSeriesReference in a TimeIntervals object and reading that copy.
23+
24+
Based on https://github.com/NeurodataWithoutBorders/pynwb/issues/1863
25+
"""
26+
new_nwb = mock_NWBFile()
27+
test_ts = mock_TimeSeries(name="test_ts", timestamps=[1.0, 2.0, 3.0], data=[1.0, 2.0, 3.0])
28+
new_nwb.add_acquisition(test_ts)
29+
new_nwb.add_trial(start_time=1.0, stop_time=2.0, timeseries=[test_ts])
30+
31+
with NWBHDF5IO(self.path1, 'w') as io:
32+
io.write(new_nwb)
33+
34+
with NWBHDF5IO(self.path1, 'r') as base_io:
35+
# the TimeIntervals object is copied but the TimeSeriesReferenceVectorData is linked
36+
nwb_add = base_io.read().copy()
37+
with NWBHDF5IO(self.path2, 'w', manager=base_io.manager) as out_io:
38+
out_io.write(nwb_add)
39+
40+
with NWBHDF5IO(self.path2, 'r') as io:
41+
nwb = io.read()
42+
ts_val = nwb.trials["timeseries"][0][0]
43+
assert isinstance(ts_val, TimeSeriesReference)
44+
assert ts_val.timeseries is nwb.acquisition["test_ts"]

tests/integration/helpers/__init__.py

Whitespace-only changes.

tests/integration/helpers/utils.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Utilities for creating a custom TypeMap for testing so that we don't use the global type map."""
2+
import tempfile
3+
from pynwb import get_type_map
4+
from pynwb.spec import NWBNamespaceBuilder, export_spec
5+
6+
7+
NAMESPACE_NAME = "test_core"
8+
9+
10+
def create_test_extension(specs, container_classes, mappers=None):
11+
ns_builder = NWBNamespaceBuilder(
12+
name=NAMESPACE_NAME,
13+
version="0.1.0",
14+
doc="test extension",
15+
)
16+
ns_builder.include_namespace("core")
17+
18+
output_dir = tempfile.TemporaryDirectory()
19+
export_spec(ns_builder, specs, output_dir.name)
20+
21+
# this will copy the global pynwb TypeMap and add the extension types to the copy
22+
type_map = get_type_map(f"{output_dir.name}/{NAMESPACE_NAME}.namespace.yaml")
23+
for type_name, container_cls in container_classes.items():
24+
type_map.register_container_type(NAMESPACE_NAME, type_name, container_cls)
25+
if mappers:
26+
for type_name, mapper_cls in mappers.items():
27+
container_cls = container_classes[type_name]
28+
type_map.register_map(container_cls, mapper_cls)
29+
30+
output_dir.cleanup()
31+
return type_map

tests/unit/test_file.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from datetime import datetime, timedelta
55
from dateutil.tz import tzlocal, tzutc
66

7+
from hdmf.common import VectorData
8+
from hdmf.utils import docval, get_docval, popargs
79
from pynwb import NWBFile, TimeSeries, NWBHDF5IO
810
from pynwb.base import Image, Images
911
from pynwb.file import Subject, ElectrodeTable, _add_missing_timezone
@@ -222,6 +224,27 @@ def test_add_trial_column(self):
222224
self.nwbfile.add_trial_column('trial_type', 'the type of trial')
223225
self.assertEqual(self.nwbfile.trials.colnames, ('start_time', 'stop_time', 'trial_type'))
224226

227+
def test_add_trial_column_custom_class(self):
228+
class SubVectorData(VectorData):
229+
__fields__ = ('extra_kwarg', )
230+
231+
@docval(
232+
*get_docval(VectorData.__init__, "name", "description", "data"),
233+
{'name': 'extra_kwarg', 'type': 'str', 'doc': 'An extra kwarg.'},
234+
)
235+
def __init__(self, **kwargs):
236+
extra_kwarg = popargs('extra_kwarg', kwargs)
237+
super().__init__(**kwargs)
238+
self.extra_kwarg = extra_kwarg
239+
240+
self.nwbfile.add_trial_column(
241+
name="test",
242+
description="test",
243+
col_cls=SubVectorData,
244+
extra_kwarg="test_extra_kwarg"
245+
)
246+
self.assertEqual(self.nwbfile.trials["test"].extra_kwarg, "test_extra_kwarg")
247+
225248
def test_add_trial(self):
226249
self.nwbfile.add_trial(start_time=10.0, stop_time=20.0)
227250
self.assertEqual(len(self.nwbfile.trials), 1)

0 commit comments

Comments
 (0)