From d2581de4a2dd3cd7609a7b411806e86a87595c26 Mon Sep 17 00:00:00 2001 From: Ryan Ly Date: Tue, 10 Jun 2025 17:42:48 -0700 Subject: [PATCH 1/5] Add 'target_tables' kwarg to DynamicTable subclasses --- src/pynwb/epoch.py | 2 +- src/pynwb/icephys.py | 17 +++++++++-------- src/pynwb/misc.py | 2 +- src/pynwb/ophys.py | 2 +- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/pynwb/epoch.py b/src/pynwb/epoch.py index 511854a9c..05eefcb99 100644 --- a/src/pynwb/epoch.py +++ b/src/pynwb/epoch.py @@ -28,7 +28,7 @@ class TimeIntervals(DynamicTable): @docval({'name': 'name', 'type': str, 'doc': 'name of this TimeIntervals'}, # required {'name': 'description', 'type': str, 'doc': 'Description of this TimeIntervals', 'default': "experimental intervals"}, - *get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames'), + *get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames', 'target_tables'), allow_positional=AllowPositional.WARNING,) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/src/pynwb/icephys.py b/src/pynwb/icephys.py index 0a1493f08..244d8b36e 100644 --- a/src/pynwb/icephys.py +++ b/src/pynwb/icephys.py @@ -422,7 +422,7 @@ class IntracellularElectrodesTable(DynamicTable): 'table': False}, ) - @docval(*get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames'), + @docval(*get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames', 'target_tables'), allow_positional=AllowPositional.WARNING,) def __init__(self, **kwargs): # Define defaultb name and description settings @@ -452,7 +452,7 @@ class IntracellularStimuliTable(DynamicTable): 'class': TimeSeriesReferenceVectorData}, ) - @docval(*get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames'), + @docval(*get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames', 'target_tables'), allow_positional=AllowPositional.WARNING,) def __init__(self, **kwargs): # Define defaultb name and description settings @@ -476,7 +476,7 @@ class IntracellularResponsesTable(DynamicTable): 'class': TimeSeriesReferenceVectorData}, ) - @docval(*get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames'), + @docval(*get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames', 'target_tables'), allow_positional=AllowPositional.WARNING,) def __init__(self, **kwargs): # Define defaultb name and description settings @@ -493,7 +493,8 @@ class IntracellularRecordingsTable(AlignedDynamicTable): a single simultaneous_recording. Each row in the table represents a single recording consisting typically of a stimulus and a corresponding response. """ - @docval(*get_docval(AlignedDynamicTable.__init__, 'id', 'columns', 'colnames', 'category_tables', 'categories'), + @docval(*get_docval(AlignedDynamicTable.__init__, 'id', 'columns', 'colnames', + 'category_tables', 'categories', 'target_tables'), allow_positional=AllowPositional.WARNING,) def __init__(self, **kwargs): kwargs['name'] = 'intracellular_recordings' @@ -782,7 +783,7 @@ class SimultaneousRecordingsTable(DynamicTable): 'reading the Container from file as the table attribute is already populated in this case ' 'but otherwise this is required.', 'default': None}, - *get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames'), + *get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames', 'target_tables'), allow_positional=AllowPositional.WARNING,) def __init__(self, **kwargs): intracellular_recordings_table = popargs('intracellular_recordings_table', kwargs) @@ -842,7 +843,7 @@ class SequentialRecordingsTable(DynamicTable): 'column indexes. May be None when reading the Container from file as the ' 'table attribute is already populated in this case but otherwise this is required.', 'default': None}, - *get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames'), + *get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames', 'target_tables'), allow_positional=AllowPositional.WARNING,) def __init__(self, **kwargs): simultaneous_recordings_table = popargs('simultaneous_recordings_table', kwargs) @@ -900,7 +901,7 @@ class RepetitionsTable(DynamicTable): 'be None when reading the Container from file as the table attribute is already populated ' 'in this case but otherwise this is required.', 'default': None}, - *get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames'), + *get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames', 'target_tables'), allow_positional=AllowPositional.WARNING,) def __init__(self, **kwargs): sequential_recordings_table = popargs('sequential_recordings_table', kwargs) @@ -953,7 +954,7 @@ class ExperimentalConditionsTable(DynamicTable): 'type': RepetitionsTable, 'doc': 'the RepetitionsTable table that the repetitions column indexes', 'default': None}, - *get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames'), + *get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames', 'target_tables'), allow_positional=AllowPositional.WARNING,) def __init__(self, **kwargs): repetitions_table = popargs('repetitions_table', kwargs) diff --git a/src/pynwb/misc.py b/src/pynwb/misc.py index 5a9b83712..0220ae40b 100644 --- a/src/pynwb/misc.py +++ b/src/pynwb/misc.py @@ -165,7 +165,7 @@ class Units(DynamicTable): ) @docval({'name': 'name', 'type': str, 'doc': 'Name of this Units interface', 'default': 'Units'}, - *get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames'), + *get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames', 'target_tables'), {'name': 'description', 'type': str, 'doc': 'a description of what is in this table', 'default': None}, {'name': 'electrode_table', 'type': DynamicTable, 'doc': 'the table that the *electrodes* column indexes', 'default': None}, diff --git a/src/pynwb/ophys.py b/src/pynwb/ophys.py index a5121f678..9113f3db0 100644 --- a/src/pynwb/ophys.py +++ b/src/pynwb/ophys.py @@ -331,7 +331,7 @@ class PlaneSegmentation(DynamicTable): {'name': 'name', 'type': str, 'doc': 'name of PlaneSegmentation.', 'default': None}, {'name': 'reference_images', 'type': (ImageSeries, list, dict, tuple), 'default': None, 'doc': 'One or more image stacks that the masks apply to (can be oneelement stack).'}, - *get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames'), + *get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames', 'target_tables'), allow_positional=AllowPositional.WARNING,) def __init__(self, **kwargs): imaging_plane, reference_images = popargs('imaging_plane', 'reference_images', kwargs) From 93624d1089e741c804f9429aa265167e3e859f14 Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Mon, 6 Oct 2025 09:57:28 -0700 Subject: [PATCH 2/5] add target_tables arg to electrodes and frequency bands table --- src/pynwb/ecephys.py | 3 ++- src/pynwb/misc.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pynwb/ecephys.py b/src/pynwb/ecephys.py index 1c781de8a..7adbe728e 100644 --- a/src/pynwb/ecephys.py +++ b/src/pynwb/ecephys.py @@ -89,7 +89,8 @@ class ElectrodesTable(DynamicTable): 'for this electrode.'), 'required': False} ) - @docval(*get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames')) + @docval(*get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames', 'target_tables'), + allow_positional=AllowPositional.WARNING,) def __init__(self, **kwargs): kwargs['name'] = 'electrodes' kwargs['description'] = 'metadata about extracellular electrodes' diff --git a/src/pynwb/misc.py b/src/pynwb/misc.py index 3b5b676ef..3a17b9f87 100644 --- a/src/pynwb/misc.py +++ b/src/pynwb/misc.py @@ -265,7 +265,8 @@ class FrequencyBandsTable(DynamicTable): {'name': 'band_stdev', 'description': 'The standard deviation Gaussian filters, in Hz.', 'required': False} ) - @docval(*get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames')) + @docval(*get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames', 'target_tables'), + allow_positional=AllowPositional.WARNING,) def __init__(self, **kwargs): kwargs['name'] = 'bands' kwargs['description'] = 'Table for describing the bands that DecompositionSeries was generated from.' From 8c93aa855ce1c23d8f1faaae9baf840f0f291015 Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Mon, 6 Oct 2025 09:57:42 -0700 Subject: [PATCH 3/5] add test for target tables --- tests/unit/test_extension.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/unit/test_extension.py b/tests/unit/test_extension.py index 6f08d4b93..af5502243 100644 --- a/tests/unit/test_extension.py +++ b/tests/unit/test_extension.py @@ -136,7 +136,42 @@ def test_lab_meta_auto(self): nwbfile = NWBFile("a file with header data", "NB123A", datetime(2017, 5, 1, 12, 0, 0, tzinfo=tzlocal())) nwbfile.add_lab_meta_data(MyTestMetaData(name='test_name', test_attr=5.)) + + def test_custom_target_table(self): + ns_builder = NWBNamespaceBuilder('Extension for custom target table', self.prefix, version='0.1.0') + test_epochs_table_ext = NWBGroupSpec( + neurodata_type_def="MyEpochsTable", + neurodata_type_inc="TimeIntervals", + doc=("Custom table for storing my epochs. Inherits from TimeIntervals."), + datasets=[ + NWBDatasetSpec( + name="my_locations", + doc="References row(s) of MyLocationsTable.", + neurodata_type_inc="DynamicTableRegion", + ), + ] + ) + test_locations_table_ext = NWBGroupSpec( + neurodata_type_def="MyLocationsTable", + neurodata_type_inc="DynamicTable", + doc=("Table to reference."), + default_name="my_locations_table", + ) + + ns_builder.add_spec(self.ext_source, test_epochs_table_ext) + ns_builder.add_spec(self.ext_source, test_locations_table_ext) + ns_builder.export(self.ns_path, outdir=self.tempdir) + ns_abs_path = os.path.join(self.tempdir, self.ns_path) + + load_namespaces(ns_abs_path) + MyLocationsTable = get_class('MyLocationsTable', self.prefix) + MyEpochsTable = get_class('MyEpochsTable', self.prefix) + my_locations_table = MyLocationsTable(name='test_name', description='test desc') + my_epochs_table = MyEpochsTable(name='test_name', + description='test desc', + target_tables={'my_locations': my_locations_table}) + self.assertIs(my_epochs_table['my_locations'].table, my_locations_table) class TestCatchDupNS(TestCase): From 9394e3e83669383e886da3b34c1622bcbf9fe8ae Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Mon, 6 Oct 2025 10:02:54 -0700 Subject: [PATCH 4/5] update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 853cbc35a..51035f458 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## PyNWB 3.1.3 (Unreleased) +### Added +- Added 'target_tables' kwarg to DynamicTable subclasses to allow classes that extend DynamicTable subclasses to specify the mapping of DynamicTableRegion columns to the target tables. @rly [#2096](https://github.com/NeurodataWithoutBorders/pynwb/issues/2096) + ### Fixed - Fixed incorrect warning for path not ending in `.nwb` when no path argument was provided. @t-b [#2130](https://github.com/NeurodataWithoutBorders/pynwb/pull/2130) - Fixed issue with setting `neurodata_type_inc` when reading NWB files with cached schema versions less than 2.2.0. @rly [#2135](https://github.com/NeurodataWithoutBorders/pynwb/pull/2135) From 27244f1c0a80a36afb8434d0b484e59f676138ff Mon Sep 17 00:00:00 2001 From: Ryan Ly Date: Mon, 6 Oct 2025 10:27:10 -0700 Subject: [PATCH 5/5] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51035f458..3234b8fff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## PyNWB 3.1.3 (Unreleased) ### Added -- Added 'target_tables' kwarg to DynamicTable subclasses to allow classes that extend DynamicTable subclasses to specify the mapping of DynamicTableRegion columns to the target tables. @rly [#2096](https://github.com/NeurodataWithoutBorders/pynwb/issues/2096) +- Added 'target_tables' kwarg to DynamicTable subclasses to allow classes that extend DynamicTable subclasses to specify the mapping of DynamicTableRegion columns to the target tables. @rly, @stephprince [#2096](https://github.com/NeurodataWithoutBorders/pynwb/issues/2096) ### Fixed - Fixed incorrect warning for path not ending in `.nwb` when no path argument was provided. @t-b [#2130](https://github.com/NeurodataWithoutBorders/pynwb/pull/2130)