diff --git a/CHANGELOG.md b/CHANGELOG.md index 533a2f650..2a8a7355f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Fix bug where namespaces were loaded in "w-" mode. @h-mayorquin [#1795](https://github.com/NeurodataWithoutBorders/pynwb/pull/1795) - Fix bug where pynwb version was reported as "unknown" to readthedocs @stephprince [#1810](https://github.com/NeurodataWithoutBorders/pynwb/pull/1810) - Fixed bug to allow linking of `TimeSeries.data` by setting the `data` constructor argument to another `TimeSeries`. @oruebel [#1766](https://github.com/NeurodataWithoutBorders/pynwb/pull/1766) +- Fix recursion error in html representation generation in jupyter notebooks. @stephprince [#1831](https://github.com/NeurodataWithoutBorders/pynwb/pull/1831) ### Documentation and tutorial enhancements - Add RemFile to streaming tutorial. @bendichter [#1761](https://github.com/NeurodataWithoutBorders/pynwb/pull/1761) diff --git a/environment-ros3.yml b/environment-ros3.yml index f5edea6ad..07838258e 100644 --- a/environment-ros3.yml +++ b/environment-ros3.yml @@ -6,7 +6,7 @@ channels: dependencies: - python==3.11 - h5py==3.8.0 - - hdmf==3.12.1 + - hdmf==3.12.2 - matplotlib==3.7.1 - numpy==1.24.2 - pandas==2.0.0 diff --git a/requirements-min.txt b/requirements-min.txt index 098aea15d..0e8bde429 100644 --- a/requirements-min.txt +++ b/requirements-min.txt @@ -1,6 +1,6 @@ # minimum versions of package dependencies for installing PyNWB h5py==2.10 # support for selection of datasets with list of indices added in 2.10 -hdmf==3.12.1 +hdmf==3.12.2 numpy==1.18 pandas==1.1.5 python-dateutil==2.7.3 diff --git a/requirements.txt b/requirements.txt index 0add9c54d..ad5d748bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # pinned dependencies to reproduce an entire development environment to use PyNWB h5py==3.10.0 -hdmf==3.12.1 +hdmf==3.12.2 numpy==1.26.1 pandas==2.1.2 python-dateutil==2.8.2 diff --git a/setup.py b/setup.py index d03688905..2a9ecb19e 100755 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ reqs = [ 'h5py>=2.10', - 'hdmf>=3.12.1', + 'hdmf>=3.12.2', 'numpy>=1.16', 'pandas>=1.1.5', 'python-dateutil>=2.7.3', diff --git a/src/pynwb/base.py b/src/pynwb/base.py index 56f909e26..e9093db52 100644 --- a/src/pynwb/base.py +++ b/src/pynwb/base.py @@ -287,6 +287,42 @@ def __get_links(self, links): def __add_link(self, links_key, link): self.fields.setdefault(links_key, list()).append(link) + def _generate_field_html(self, key, value, level, access_code): + def find_location_in_memory_nwbfile(current_location: str, neurodata_object) -> str: + """ + Method for determining the location of a neurodata object within an in-memory NWBFile object. Adapted from + neuroconv package. + """ + parent = neurodata_object.parent + if parent is None: + return neurodata_object.name + "/" + current_location + elif parent.name == 'root': + # Items in defined top-level places like acquisition, intervals, etc. do not act as 'containers' + # in that they do not set the `.parent` attribute; ask if object is in their in-memory dictionaries + # instead + for parent_field_name, parent_field_value in parent.fields.items(): + if isinstance(parent_field_value, dict) and neurodata_object.name in parent_field_value: + return parent_field_name + "/" + neurodata_object.name + "/" + current_location + return neurodata_object.name + "/" + current_location + return find_location_in_memory_nwbfile( + current_location=neurodata_object.name + "/" + current_location, neurodata_object=parent + ) + + # reassign value if linked timestamp or linked data to avoid recursion error + if key in ['timestamps', 'data'] and isinstance(value, TimeSeries): + path_to_linked_object = find_location_in_memory_nwbfile(key, value) + if key == 'timestamps': + value = value.timestamps + elif key == 'data': + value = value.data + key = f'{key} (link to {path_to_linked_object})' + + if key in ['timestamp_link', 'data_link']: + linked_key = 'timestamps' if key == 'timestamp_link' else 'data' + value = [find_location_in_memory_nwbfile(linked_key, v) for v in value] + + return super()._generate_field_html(key, value, level, access_code) + @property def time_unit(self): return self.__time_unit diff --git a/tests/unit/test_base.py b/tests/unit/test_base.py index ad4ce6739..f8c08f68f 100644 --- a/tests/unit/test_base.py +++ b/tests/unit/test_base.py @@ -464,6 +464,20 @@ def test_file_with_starting_time_and_timestamps_in_construct_mode(self): timestamps=[1, 2, 3, 4, 5] ) + def test_repr_html(self): + """ Test that html representation of linked timestamp data will occur as expected and will not cause a + RecursionError + """ + data1 = [0, 1, 2, 3] + data2 = [4, 5, 6, 7] + timestamps = [0.0, 0.1, 0.2, 0.3] + ts1 = TimeSeries(name="test_ts1", data=data1, unit="grams", timestamps=timestamps) + ts2 = TimeSeries(name="test_ts2", data=data2, unit="grams", timestamps=ts1) + pm = ProcessingModule(name="processing", description="a test processing module") + pm.add(ts1) + pm.add(ts2) + self.assertIn('(link to processing/test_ts1/timestamps)', pm._repr_html_()) + class TestImage(TestCase): def test_init(self):