From c327cfa76a0c2a64d962cf0796c0ba2108c253c5 Mon Sep 17 00:00:00 2001 From: mavaylon1 Date: Tue, 8 Jul 2025 16:26:33 -0700 Subject: [PATCH 01/20] HERD Changes internal file --- src/pynwb/file.py | 30 +++++- src/pynwb/io/file.py | 2 + src/pynwb/nwb-schema | 2 +- tests/unit/test_resources.py | 192 +++++++++++++++++++++++++++++++++++ 4 files changed, 224 insertions(+), 2 deletions(-) diff --git a/src/pynwb/file.py b/src/pynwb/file.py index c2fd317ca..d98c4604d 100644 --- a/src/pynwb/file.py +++ b/src/pynwb/file.py @@ -339,6 +339,7 @@ class NWBFile(MultiContainerInterface, HERDManager): {'name': 'keywords', 'type': 'array_data', 'doc': 'Terms to search over', 'default': None}, {'name': 'notes', 'type': str, 'doc': 'Notes about the experiment.', 'default': None}, + {'name': 'external_resources', 'child': True, 'required_name': 'external_resources'}, {'name': 'pharmacology', 'type': str, 'doc': 'Description of drugs used, including how and when they were administered. ' 'Anesthesia(s), painkiller(s), etc., plus dosage, concentration, etc.', 'default': None}, @@ -483,9 +484,13 @@ def __init__(self, **kwargs): 'icephys_experimental_conditions' ] args_to_set = popargs_to_dict(keys_to_set, kwargs) + args_to_set['internal_herd'] = popargs('external_resources', kwargs) kwargs['name'] = 'root' super().__init__(**kwargs) + self.reset_herd = False + self.external_herd = None + # add timezone to session_start_time if missing session_start_time = args_to_set['session_start_time'] if session_start_time.tzinfo is None: @@ -570,6 +575,29 @@ def all_children(self): stack.append(c) return ret + def link_resources(self, herd): + """ + This method is to set an external HERD file as the external resources for this file. + This will not persist on export. # TODO: This could change in the future with further development. + """ + self.external_herd = herd + self.reset_herd = True + + @property + def external_resources(self): + if self.reset_herd: + return self.external_herd + else: + return self.internal_herd + + @external_resources.setter + def external_resources(self, herd): + """ + This is here to set HERD for the file if the user did not do so using __init__. + """ + self.internal_herd = herd + self.internal_herd.parent = self + @property def objects(self): if self.__obj is None: @@ -1152,4 +1180,4 @@ def ElectrodeTable(name='electrodes', description='metadata about extracellular electrodes'): warn("The ElectrodeTable convenience function is deprecated. Please create a new instance of " "the ElectrodesTable class instead.", DeprecationWarning) - return ElectrodesTable() \ No newline at end of file + return ElectrodesTable() diff --git a/src/pynwb/io/file.py b/src/pynwb/io/file.py index d74c66be1..a3233a01d 100644 --- a/src/pynwb/io/file.py +++ b/src/pynwb/io/file.py @@ -110,6 +110,8 @@ def __init__(self, spec): self.map_spec('subject', general_spec.get_group('subject')) + self.map_spec('external_resources', general_spec.get_group('external_resources')) + device_spec = general_spec.get_group('devices') self.unmap(device_spec) self.map_spec('devices', device_spec.get_neurodata_type('Device')) diff --git a/src/pynwb/nwb-schema b/src/pynwb/nwb-schema index ade50ef33..39aaaec2b 160000 --- a/src/pynwb/nwb-schema +++ b/src/pynwb/nwb-schema @@ -1 +1 @@ -Subproject commit ade50ef33446beb3c7df4c6f1072ae0e821b5115 +Subproject commit 39aaaec2b199f7509c60da4a6287c5df68c96259 diff --git a/tests/unit/test_resources.py b/tests/unit/test_resources.py index 108a7fd84..85d9279f0 100644 --- a/tests/unit/test_resources.py +++ b/tests/unit/test_resources.py @@ -1,10 +1,27 @@ import warnings +from datetime import datetime +from uuid import uuid4 +import os +import numpy as np + +from dateutil import tz from pynwb.resources import HERD +from pynwb.file import Subject +from pynwb import NWBHDF5IO, NWBFile from pynwb.testing import TestCase class TestNWBContainer(TestCase): + def setUp(self): + self.path = "resources_file.nwb" + self.export_path = "export_file.nwb" + + def tearDown(self): + for path in [self.path, self.export_path]: + if os.path.isfile(path): + os.remove(path) + def test_constructor(self): """ Test constructor @@ -17,3 +34,178 @@ def test_constructor(self): ) er = HERD() self.assertIsInstance(er, HERD) + + def test_nwbfile_init_herd(self): + session_start_time = datetime(2018, 4, 25, 2, 30, 3, tzinfo=tz.gettz("US/Pacific")) + herd = HERD() + nwbfile = NWBFile( + session_description="A Person undergoing brain pokes.", + identifier=str(uuid4()), + session_start_time=session_start_time, + external_resources=herd + ) + self.assertTrue(isinstance(nwbfile.external_resources, HERD)) + + def test_nwbfile_set_herd(self): + session_start_time = datetime(2018, 4, 25, 2, 30, 3, tzinfo=tz.gettz("US/Pacific")) + herd = HERD() + nwbfile = NWBFile( + session_description="A Person undergoing brain pokes.", + identifier=str(uuid4()), + session_start_time=session_start_time, + ) + nwbfile.external_resources = herd + self.assertTrue(isinstance(nwbfile.external_resources, HERD)) + self.assertEqual(nwbfile.external_resources.parent, nwbfile) + + def test_resources_roundtrip(self): + session_start_time = datetime(2018, 4, 25, 2, 30, 3, tzinfo=tz.gettz("US/Pacific")) + + nwbfile = NWBFile( + session_description="A Person undergoing brain pokes.", + identifier=str(uuid4()), + session_start_time=session_start_time, + ) + subject = Subject( + subject_id="001", + age="26", + description="human 5", + species='Homo sapiens', + sex="M", + ) + + nwbfile.subject = subject + herd = HERD() + nwbfile.external_resources = herd + + nwbfile.external_resources.add_ref(container=nwbfile.subject, + key=nwbfile.subject.species, + entity_id="NCBI_TAXON:9606", + entity_uri='https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?mode=Info&id=9606') + + with NWBHDF5IO(self.path, "w") as io: + io.write(nwbfile) + + with NWBHDF5IO(self.path, "r") as io: + read_nwbfile = io.read() + self.assertEqual( + read_nwbfile.external_resources.keys[:], + np.array( + [[(b'Homo sapiens',)]], + dtype=[('key', 'O')] + ) + ) + + self.assertEqual( + read_nwbfile.external_resources.entities[:], + np.array( + [ + ('NCBI_TAXON:9606', + 'https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?mode=Info&id=9606') + ], + dtype=[('entity_id', 'O'), ('entity_uri', 'O')] + ) + ) + + self.assertEqual( + read_nwbfile.external_resources.objects[:], + np.array( + [ + (0, + subject.object_id, + 'Subject', + '', + '') + ], + dtype=[ + ('files_idx', ' Date: Tue, 8 Jul 2025 16:54:41 -0700 Subject: [PATCH 02/20] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 282156763..1154441f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # PyNWB Changelog +## PyNWB 3.1.0 (Upcoming) +### Enhancements and minor changes +- Added HERD to to `general` within the the `NWBFile`. (https://github.com/NeurodataWithoutBorders/pynwb/pull/2111) + ## PyNWB 3.1.0 (July 8, 2025) ### Breaking changes From 2c97c0d17ad5df5ed25c4ae17c20266a926b09b6 Mon Sep 17 00:00:00 2001 From: Matthew Avaylon Date: Tue, 8 Jul 2025 17:11:49 -0700 Subject: [PATCH 03/20] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1154441f2..9c59cf953 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## PyNWB 3.1.0 (Upcoming) ### Enhancements and minor changes -- Added HERD to to `general` within the the `NWBFile`. (https://github.com/NeurodataWithoutBorders/pynwb/pull/2111) +- Added HERD to to `general` within the the `NWBFile`. @mavaylon1 [#2111](https://github.com/NeurodataWithoutBorders/pynwb/pull/2111) ## PyNWB 3.1.0 (July 8, 2025) From dc71149d5cc46a27339915951f4a24c8015121bc Mon Sep 17 00:00:00 2001 From: Ben Dichter Date: Thu, 19 Feb 2026 22:41:00 -0500 Subject: [PATCH 04/20] Fix external_resources docval and update nwb-schema submodule Move 'child' and 'required_name' keys from @docval to __nwbfields__ where they belong, fixing the import-time crash: "docval for external_resources: keys ['child', 'required_name'] are not supported by docval" Also update nwb-schema submodule to latest herd branch (with dev merge conflict resolved). Co-Authored-By: Claude Opus 4.6 --- src/pynwb/file.py | 5 ++++- src/pynwb/nwb-schema | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pynwb/file.py b/src/pynwb/file.py index d98c4604d..39570048b 100644 --- a/src/pynwb/file.py +++ b/src/pynwb/file.py @@ -9,6 +9,7 @@ from hdmf.common import DynamicTableRegion, DynamicTable from hdmf.container import HERDManager +from hdmf.common import HERD from hdmf.utils import docval, getargs, get_docval, popargs, popargs_to_dict, AllowPositional from . import register_class, CORE_NAMESPACE @@ -287,6 +288,7 @@ class NWBFile(MultiContainerInterface, HERDManager): {'name': 'trials', 'child': True, 'required_name': 'trials'}, {'name': 'units', 'child': True, 'required_name': 'units'}, {'name': 'subject', 'child': True, 'required_name': 'subject'}, + {'name': 'external_resources', 'child': True, 'required_name': 'external_resources'}, {'name': 'sweep_table', 'child': True, 'required_name': 'sweep_table'}, {'name': 'invalid_times', 'child': True, 'required_name': 'invalid_times'}, # icephys_filtering is temporary. /intracellular_ephys/filtering dataset will be deprecated @@ -339,7 +341,8 @@ class NWBFile(MultiContainerInterface, HERDManager): {'name': 'keywords', 'type': 'array_data', 'doc': 'Terms to search over', 'default': None}, {'name': 'notes', 'type': str, 'doc': 'Notes about the experiment.', 'default': None}, - {'name': 'external_resources', 'child': True, 'required_name': 'external_resources'}, + {'name': 'external_resources', 'type': HERD, + 'doc': 'the HERD external resources object for this NWBFile', 'default': None}, {'name': 'pharmacology', 'type': str, 'doc': 'Description of drugs used, including how and when they were administered. ' 'Anesthesia(s), painkiller(s), etc., plus dosage, concentration, etc.', 'default': None}, diff --git a/src/pynwb/nwb-schema b/src/pynwb/nwb-schema index 39aaaec2b..77642b99f 160000 --- a/src/pynwb/nwb-schema +++ b/src/pynwb/nwb-schema @@ -1 +1 @@ -Subproject commit 39aaaec2b199f7509c60da4a6287c5df68c96259 +Subproject commit 77642b99f3d1bd09b5a8c1b947c1e6965ecd308a From c97a8e392f4a52d770fc8ed0247fd43da97ab049 Mon Sep 17 00:00:00 2001 From: Ben Dichter Date: Thu, 19 Feb 2026 22:49:45 -0500 Subject: [PATCH 05/20] Update nwb-schema submodule to include hdmf-common-schema 1.9.0 The nwb-schema herd branch now includes hdmf-common-schema 1.9.0, which moved HERD from hdmf-experimental to hdmf-common. This is required so the NWB core namespace can find the HERD type. Note: This also requires hdmf >= 4.3.2 (unreleased) which bundles hdmf-common-schema 1.9.0. CI should install hdmf from dev branch until 4.3.2 is released. Co-Authored-By: Claude Opus 4.6 --- src/pynwb/nwb-schema | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pynwb/nwb-schema b/src/pynwb/nwb-schema index 77642b99f..d15dfb130 160000 --- a/src/pynwb/nwb-schema +++ b/src/pynwb/nwb-schema @@ -1 +1 @@ -Subproject commit 77642b99f3d1bd09b5a8c1b947c1e6965ecd308a +Subproject commit d15dfb130a280d9d4882ef340ed6e780055d852b From 2c3afeca6acfe2f9c804722df8748ca04aa56c88 Mon Sep 17 00:00:00 2001 From: Ben Dichter Date: Thu, 19 Feb 2026 23:05:05 -0500 Subject: [PATCH 06/20] Pin hdmf>=5.0.0 (requires hdmf-common-schema 1.9.0 for HERD) hdmf 5.0.0 will bundle hdmf-common-schema 1.9.0, which moved HERD from the hdmf-experimental namespace to hdmf-common. This is required for the NWB core namespace to resolve the HERD type used in nwb.file.yaml. Co-Authored-By: Claude Opus 4.6 --- pyproject.toml | 2 +- requirements-min.txt | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 24a6cb405..4fcb9f89c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ classifiers = [ ] dependencies = [ "h5py>=3.2.0", - "hdmf>=4.1.2,<5", + "hdmf>=5.0.0,<6", "numpy>=1.24.0", "pandas>=1.2.0", "python-dateutil>=2.8.2", diff --git a/requirements-min.txt b/requirements-min.txt index 61241e7c6..6bbe0d1ba 100644 --- a/requirements-min.txt +++ b/requirements-min.txt @@ -1,6 +1,6 @@ # minimum versions of package dependencies for installing PyNWB h5py==3.2.0 -hdmf==4.1.2 +hdmf==5.0.0 numpy==1.24.0 pandas==1.2.0 python-dateutil==2.8.2 diff --git a/requirements.txt b/requirements.txt index 79b6db693..be92e06f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # pinned dependencies to reproduce an entire development environment to use PyNWB h5py==3.12.1 -hdmf==4.1.2 +hdmf==5.0.0 numpy==2.1.1; python_version > "3.9" # numpy 2.1+ is not compatible with py3.9 numpy==2.0.2; python_version == "3.9" pandas==2.2.3 From eaae41b5b6d46e77cda6cd802f05849b9faf5055 Mon Sep 17 00:00:00 2001 From: rly <310197+rly@users.noreply.github.com> Date: Thu, 5 Mar 2026 22:08:58 -0800 Subject: [PATCH 07/20] Improve NWBFile external_resources HERD integration - Rename external_herd/internal_herd to _external_herd/_internal_herd - Use external_resources setter in __init__ to properly set parent - Remove reset_herd flag; check _external_herd is not None instead - Add docstrings to external_resources property, setter, and link_resources - Fix test assertions for HERD keys array shape (1-D not 2-D) Co-Authored-By: Claude Opus 4.6 --- src/pynwb/file.py | 41 +++++++++++++++++++++--------------- tests/unit/test_resources.py | 4 ++-- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/pynwb/file.py b/src/pynwb/file.py index 39570048b..ffa8ad1b0 100644 --- a/src/pynwb/file.py +++ b/src/pynwb/file.py @@ -487,12 +487,15 @@ def __init__(self, **kwargs): 'icephys_experimental_conditions' ] args_to_set = popargs_to_dict(keys_to_set, kwargs) - args_to_set['internal_herd'] = popargs('external_resources', kwargs) + external_resources = popargs('external_resources', kwargs) kwargs['name'] = 'root' super().__init__(**kwargs) - self.reset_herd = False - self.external_herd = None + self._external_herd = None + self._internal_herd = None + + if external_resources is not None: + self.external_resources = external_resources # add timezone to session_start_time if missing session_start_time = args_to_set['session_start_time'] @@ -579,27 +582,31 @@ def all_children(self): return ret def link_resources(self, herd): + """Link an external HERD object as the external resources for this file. + + The linked HERD will be returned by the ``external_resources`` property + but will not be written on export; the original internal HERD (if any) + is preserved in the exported file. """ - This method is to set an external HERD file as the external resources for this file. - This will not persist on export. # TODO: This could change in the future with further development. - """ - self.external_herd = herd - self.reset_herd = True + self._external_herd = herd @property def external_resources(self): - if self.reset_herd: - return self.external_herd - else: - return self.internal_herd + """Return the HERD external resources object for this NWBFile. + + If an external HERD has been linked via ``link_resources``, that object + is returned. Otherwise, the internal HERD set via ``__init__`` or the + setter is returned. + """ + if self._external_herd is not None: + return self._external_herd + return self._internal_herd @external_resources.setter def external_resources(self, herd): - """ - This is here to set HERD for the file if the user did not do so using __init__. - """ - self.internal_herd = herd - self.internal_herd.parent = self + """Set the internal HERD external resources object for this NWBFile.""" + self._internal_herd = herd + self._internal_herd.parent = self @property def objects(self): diff --git a/tests/unit/test_resources.py b/tests/unit/test_resources.py index 85d9279f0..ec07f24a4 100644 --- a/tests/unit/test_resources.py +++ b/tests/unit/test_resources.py @@ -91,7 +91,7 @@ def test_resources_roundtrip(self): self.assertEqual( read_nwbfile.external_resources.keys[:], np.array( - [[(b'Homo sapiens',)]], + [('Homo sapiens',)], dtype=[('key', 'O')] ) ) @@ -174,7 +174,7 @@ def test_link_resources(self): self.assertEqual( read_export_nwbfile.external_resources.keys[:], np.array( - [[(b'Homo sapiens',)]], + [('Homo sapiens',)], dtype=[('key', 'O')] ) ) From 285eac2b9de87656bbf244bc01a8f7ab8830521e Mon Sep 17 00:00:00 2001 From: Ryan Ly <310197+rly@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:06:16 -0700 Subject: [PATCH 08/20] Update hdmf req to 5.0.1 --- pyproject.toml | 2 +- requirements-min.txt | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 584c81291..ccf66a857 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ classifiers = [ ] dependencies = [ "h5py>=3.6.0", - "hdmf>=5.0.0,<6", + "hdmf>=5.0.1,<6", "numpy>=1.24.0", "pandas>=1.3.5", "python-dateutil>=2.8.2", diff --git a/requirements-min.txt b/requirements-min.txt index 34430cd1e..14bcba7d6 100644 --- a/requirements-min.txt +++ b/requirements-min.txt @@ -1,6 +1,6 @@ # minimum versions of package dependencies for installing PyNWB h5py==3.6.0 -hdmf==5.0.0 +hdmf==5.0.1 numpy==1.24.0 pandas==1.3.5 python-dateutil==2.8.2 diff --git a/requirements.txt b/requirements.txt index 0b646186b..a7ece46c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # pinned dependencies to reproduce an entire development environment to use PyNWB h5py==3.12.1 -hdmf==5.0.0 +hdmf==5.0.1 numpy==2.1.1 pandas==2.2.3 python-dateutil==2.9.0.post0 From a99004275eeb69beb42bdb4380118aca3127775e Mon Sep 17 00:00:00 2001 From: Ryan Ly <310197+rly@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:24:44 -0700 Subject: [PATCH 09/20] Update hdmf conda dep --- environment-ros3.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment-ros3.yml b/environment-ros3.yml index e4bcbdcd9..fd3deea65 100644 --- a/environment-ros3.yml +++ b/environment-ros3.yml @@ -6,6 +6,7 @@ channels: dependencies: - python==3.14 - h5py==3.15.1 + - hdmf==5.0.1 - matplotlib==3.10.8 - numpy==2.4.2 - pandas==3.0.1 @@ -17,6 +18,5 @@ dependencies: - aiohttp==3.13.3 - pip - pip: - - hdmf==5.0.0 # not yet available on conda-forge - remfile==0.1.13 - dandi==0.62.1 # NOTE: dandi is not available on conda for osx-arm64 From 3d5b1cb9d2163419cd4af39677a07093e1830e73 Mon Sep 17 00:00:00 2001 From: Ryan Ly <310197+rly@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:17:42 -0700 Subject: [PATCH 10/20] Apply suggestion from @rly --- src/pynwb/file.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pynwb/file.py b/src/pynwb/file.py index ffa8ad1b0..ac16299ae 100644 --- a/src/pynwb/file.py +++ b/src/pynwb/file.py @@ -495,6 +495,7 @@ def __init__(self, **kwargs): self._internal_herd = None if external_resources is not None: + # NOTE: this sets self._internal_herd self.external_resources = external_resources # add timezone to session_start_time if missing From accdafd5113a7d6c3ac5af41cf60bed08341fbf7 Mon Sep 17 00:00:00 2001 From: rly <310197+rly@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:30:32 -0700 Subject: [PATCH 11/20] Update HERD changelog entry, improve test cleanup and descriptions - Fix grammar and enhance HERD changelog entry, add @rly - Use tempfile.TemporaryDirectory for test file cleanup - Use realistic session_description in tests Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 3 ++- tests/unit/test_resources.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49c85d8ce..d70e6b340 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - Fixed broken and redirecting links in documentation. @bendichter [#2165](https://github.com/NeurodataWithoutBorders/pynwb/pull/2165) ### Added -- Added HERD to to `general` within the the `NWBFile`. @mavaylon1 [#2111](https://github.com/NeurodataWithoutBorders/pynwb/pull/2111) +- Added support for HERD (HDMF External Resources Data Structure) as the `external_resources` field on `NWBFile`, enabling users to associate external resource annotations (e.g., ontology term mappings) with their NWB files. Also added `NWBFile.link_resources` for linking an external HERD object. @mavaylon1, @rly [#2111](https://github.com/NeurodataWithoutBorders/pynwb/pull/2111) - Added `get_starting_time()` and `get_duration()` methods to `TimeSeries` to get the starting time and duration of the time series. @h-mayorquin [#2146](https://github.com/NeurodataWithoutBorders/pynwb/pull/2146) - Added `get_starting_time()` and `get_duration()` methods to `TimeIntervals` to get the earliest start time and total duration (span from earliest start to latest stop) of all intervals. @h-mayorquin [#2146](https://github.com/NeurodataWithoutBorders/pynwb/pull/2146) - Added `get_starting_time()` and `get_duration()` methods to `Units` to get the earliest spike time and total duration (span from earliest to latest spike) across all units. @h-mayorquin [#2164](https://github.com/NeurodataWithoutBorders/pynwb/pull/2164) @@ -60,6 +60,7 @@ - Updated tests to skip streaming tests gracefully if offline. @rly [#2113](https://github.com/NeurodataWithoutBorders/pynwb/pull/2113) - Added check in `PlaneSegmentation` constructor for required columns. @rly [#2102](https://github.com/NeurodataWithoutBorders/pynwb/pull/2102) + ## PyNWB 3.1.0 (July 8, 2025) ### Breaking changes diff --git a/tests/unit/test_resources.py b/tests/unit/test_resources.py index ec07f24a4..8673668d7 100644 --- a/tests/unit/test_resources.py +++ b/tests/unit/test_resources.py @@ -1,3 +1,4 @@ +import tempfile import warnings from datetime import datetime from uuid import uuid4 @@ -14,13 +15,12 @@ class TestNWBContainer(TestCase): def setUp(self): - self.path = "resources_file.nwb" - self.export_path = "export_file.nwb" + self.tmpdir = tempfile.TemporaryDirectory() + self.path = os.path.join(self.tmpdir.name, "resources_file.nwb") + self.export_path = os.path.join(self.tmpdir.name, "export_file.nwb") def tearDown(self): - for path in [self.path, self.export_path]: - if os.path.isfile(path): - os.remove(path) + self.tmpdir.cleanup() def test_constructor(self): """ @@ -39,7 +39,7 @@ def test_nwbfile_init_herd(self): session_start_time = datetime(2018, 4, 25, 2, 30, 3, tzinfo=tz.gettz("US/Pacific")) herd = HERD() nwbfile = NWBFile( - session_description="A Person undergoing brain pokes.", + session_description="ECoG recording during audio speech perception task", identifier=str(uuid4()), session_start_time=session_start_time, external_resources=herd @@ -50,7 +50,7 @@ def test_nwbfile_set_herd(self): session_start_time = datetime(2018, 4, 25, 2, 30, 3, tzinfo=tz.gettz("US/Pacific")) herd = HERD() nwbfile = NWBFile( - session_description="A Person undergoing brain pokes.", + session_description="ECoG recording during audio speech perception task", identifier=str(uuid4()), session_start_time=session_start_time, ) @@ -62,7 +62,7 @@ def test_resources_roundtrip(self): session_start_time = datetime(2018, 4, 25, 2, 30, 3, tzinfo=tz.gettz("US/Pacific")) nwbfile = NWBFile( - session_description="A Person undergoing brain pokes.", + session_description="ECoG recording during audio speech perception task", identifier=str(uuid4()), session_start_time=session_start_time, ) @@ -134,7 +134,7 @@ def test_link_resources(self): session_start_time = datetime(2018, 4, 25, 2, 30, 3, tzinfo=tz.gettz("US/Pacific")) nwbfile = NWBFile( - session_description="A Person undergoing brain pokes.", + session_description="ECoG recording during audio speech perception task", identifier=str(uuid4()), session_start_time=session_start_time, ) From 76a7f3b476ee59aa62f7a42834ee34fc5bf553d2 Mon Sep 17 00:00:00 2001 From: rly <310197+rly@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:47:29 -0700 Subject: [PATCH 12/20] Clean up HERD code and tests - Combine duplicate hdmf.common imports in file.py - Extract shared test setup into _create_nwbfile_with_herd helper - Fix import ordering, quote consistency, and indentation in tests - Use ISO 8601 duration format for age field Co-Authored-By: Claude Opus 4.6 --- src/pynwb/file.py | 3 +- tests/unit/test_resources.py | 178 ++++++++++++++--------------------- 2 files changed, 74 insertions(+), 107 deletions(-) diff --git a/src/pynwb/file.py b/src/pynwb/file.py index ac16299ae..642a9ea25 100644 --- a/src/pynwb/file.py +++ b/src/pynwb/file.py @@ -7,9 +7,8 @@ import numpy as np import pandas as pd -from hdmf.common import DynamicTableRegion, DynamicTable +from hdmf.common import DynamicTableRegion, DynamicTable, HERD from hdmf.container import HERDManager -from hdmf.common import HERD from hdmf.utils import docval, getargs, get_docval, popargs, popargs_to_dict, AllowPositional from . import register_class, CORE_NAMESPACE diff --git a/tests/unit/test_resources.py b/tests/unit/test_resources.py index 8673668d7..6dceb5050 100644 --- a/tests/unit/test_resources.py +++ b/tests/unit/test_resources.py @@ -1,15 +1,15 @@ +import os import tempfile import warnings from datetime import datetime from uuid import uuid4 -import os -import numpy as np +import numpy as np from dateutil import tz -from pynwb.resources import HERD -from pynwb.file import Subject from pynwb import NWBHDF5IO, NWBFile +from pynwb.file import Subject +from pynwb.resources import HERD from pynwb.testing import TestCase @@ -22,6 +22,32 @@ def setUp(self): def tearDown(self): self.tmpdir.cleanup() + def _create_nwbfile_with_herd(self): + """Create an NWBFile with a Subject and HERD containing a species annotation.""" + session_start_time = datetime(2018, 4, 25, 2, 30, 3, tzinfo=tz.gettz("US/Pacific")) + nwbfile = NWBFile( + session_description="ECoG recording during audio speech perception task", + identifier=str(uuid4()), + session_start_time=session_start_time, + ) + subject = Subject( + subject_id="001", + age="P26Y", + description="human subject", + species="Homo sapiens", + sex="M", + ) + nwbfile.subject = subject + herd = HERD() + nwbfile.external_resources = herd + nwbfile.external_resources.add_ref( + container=nwbfile.subject, + key=nwbfile.subject.species, + entity_id="NCBI_TAXON:9606", + entity_uri="https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?mode=Info&id=9606", + ) + return nwbfile, subject + def test_constructor(self): """ Test constructor @@ -42,9 +68,9 @@ def test_nwbfile_init_herd(self): session_description="ECoG recording during audio speech perception task", identifier=str(uuid4()), session_start_time=session_start_time, - external_resources=herd + external_resources=herd, ) - self.assertTrue(isinstance(nwbfile.external_resources, HERD)) + self.assertIsInstance(nwbfile.external_resources, HERD) def test_nwbfile_set_herd(self): session_start_time = datetime(2018, 4, 25, 2, 30, 3, tzinfo=tz.gettz("US/Pacific")) @@ -55,33 +81,11 @@ def test_nwbfile_set_herd(self): session_start_time=session_start_time, ) nwbfile.external_resources = herd - self.assertTrue(isinstance(nwbfile.external_resources, HERD)) + self.assertIsInstance(nwbfile.external_resources, HERD) self.assertEqual(nwbfile.external_resources.parent, nwbfile) def test_resources_roundtrip(self): - session_start_time = datetime(2018, 4, 25, 2, 30, 3, tzinfo=tz.gettz("US/Pacific")) - - nwbfile = NWBFile( - session_description="ECoG recording during audio speech perception task", - identifier=str(uuid4()), - session_start_time=session_start_time, - ) - subject = Subject( - subject_id="001", - age="26", - description="human 5", - species='Homo sapiens', - sex="M", - ) - - nwbfile.subject = subject - herd = HERD() - nwbfile.external_resources = herd - - nwbfile.external_resources.add_ref(container=nwbfile.subject, - key=nwbfile.subject.species, - entity_id="NCBI_TAXON:9606", - entity_uri='https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?mode=Info&id=9606') + nwbfile, subject = self._create_nwbfile_with_herd() with NWBHDF5IO(self.path, "w") as io: io.write(nwbfile) @@ -91,74 +95,44 @@ def test_resources_roundtrip(self): self.assertEqual( read_nwbfile.external_resources.keys[:], np.array( - [('Homo sapiens',)], - dtype=[('key', 'O')] - ) + [("Homo sapiens",)], + dtype=[("key", "O")], + ), ) - self.assertEqual( read_nwbfile.external_resources.entities[:], np.array( [ - ('NCBI_TAXON:9606', - 'https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?mode=Info&id=9606') + ( + "NCBI_TAXON:9606", + "https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?mode=Info&id=9606", + ) ], - dtype=[('entity_id', 'O'), ('entity_uri', 'O')] - ) + dtype=[("entity_id", "O"), ("entity_uri", "O")], + ), ) - self.assertEqual( read_nwbfile.external_resources.objects[:], np.array( - [ - (0, - subject.object_id, - 'Subject', - '', - '') - ], + [(0, subject.object_id, "Subject", "", "")], dtype=[ - ('files_idx', ' Date: Tue, 17 Mar 2026 11:57:03 -0700 Subject: [PATCH 13/20] Limit pandas<3 to match hdmf dependency Co-Authored-By: Claude Opus 4.6 --- environment-ros3.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/environment-ros3.yml b/environment-ros3.yml index fd3deea65..3244d41fc 100644 --- a/environment-ros3.yml +++ b/environment-ros3.yml @@ -9,7 +9,7 @@ dependencies: - hdmf==5.0.1 - matplotlib==3.10.8 - numpy==2.4.2 - - pandas==3.0.1 + - pandas==2.3.3 - python-dateutil==2.9.0 - setuptools - pytest==9.0.2 diff --git a/pyproject.toml b/pyproject.toml index ceb549b54..4c613f3e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ "h5py>=3.6.0", "hdmf>=5.0.1,<6", "numpy>=1.24.0", - "pandas>=1.4.0", + "pandas>=1.4.0,<3", "python-dateutil>=2.8.2", "platformdirs>=4.2.2" ] From 6b53d3c1fab656ecfe0482600b5608760c87ee4e Mon Sep 17 00:00:00 2001 From: rly <310197+rly@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:03:43 -0700 Subject: [PATCH 14/20] Update dandi to 0.74.3 in ROS3 environment Server requires at least version 0.74.0. Co-Authored-By: Claude Opus 4.6 --- environment-ros3.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment-ros3.yml b/environment-ros3.yml index 3244d41fc..80fd3cd51 100644 --- a/environment-ros3.yml +++ b/environment-ros3.yml @@ -19,4 +19,4 @@ dependencies: - pip - pip: - remfile==0.1.13 - - dandi==0.62.1 # NOTE: dandi is not available on conda for osx-arm64 + - dandi==0.74.3 # NOTE: dandi is not available on conda for osx-arm64 From 3232df4b194122b992aec74376090d04624f1fe8 Mon Sep 17 00:00:00 2001 From: rly <310197+rly@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:16:16 -0700 Subject: [PATCH 15/20] Update submodule to dev --- src/pynwb/nwb-schema | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pynwb/nwb-schema b/src/pynwb/nwb-schema index d15dfb130..a6b23f073 160000 --- a/src/pynwb/nwb-schema +++ b/src/pynwb/nwb-schema @@ -1 +1 @@ -Subproject commit d15dfb130a280d9d4882ef340ed6e780055d852b +Subproject commit a6b23f073f749538eccd33eebcc25533afd0ad08 From a84b134fb1e13f751de7b1e0343c43a9e775987f Mon Sep 17 00:00:00 2001 From: rly <310197+rly@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:18:29 -0700 Subject: [PATCH 16/20] Apply PR review feedback for HERD integration - Rename _external_herd/_internal_herd to _linked_external_resources/_external_resources - Rename herd parameter to external_resources in link_resources and setter - Add get_external_resources(linked) method to access specific HERD - Add test for get_external_resources Co-Authored-By: Claude Opus 4.6 --- src/pynwb/file.py | 48 +++++++++++++++++++++++------------- test_herd_access.py | 47 +++++++++++++++++++++++++++++++++++ tests/unit/test_resources.py | 15 ++++++++++- 3 files changed, 92 insertions(+), 18 deletions(-) create mode 100644 test_herd_access.py diff --git a/src/pynwb/file.py b/src/pynwb/file.py index 642a9ea25..3afe85e20 100644 --- a/src/pynwb/file.py +++ b/src/pynwb/file.py @@ -490,11 +490,11 @@ def __init__(self, **kwargs): kwargs['name'] = 'root' super().__init__(**kwargs) - self._external_herd = None - self._internal_herd = None + self._linked_external_resources = None + self._external_resources = None if external_resources is not None: - # NOTE: this sets self._internal_herd + # NOTE: this sets self._external_resources via the property setter self.external_resources = external_resources # add timezone to session_start_time if missing @@ -581,32 +581,46 @@ def all_children(self): stack.append(c) return ret - def link_resources(self, herd): + def link_resources(self, external_resources): """Link an external HERD object as the external resources for this file. The linked HERD will be returned by the ``external_resources`` property - but will not be written on export; the original internal HERD (if any) - is preserved in the exported file. + but will not be written on export; the original HERD (if any) is + preserved in the exported file. """ - self._external_herd = herd + self._linked_external_resources = external_resources + + def get_external_resources(self, linked=False): + """Return the HERD external resources object for this NWBFile. + + Parameters + ---------- + linked : bool, optional + If True, return the linked HERD set via ``link_resources``. + If False (default), return the HERD set via ``__init__`` or the + ``external_resources`` setter. + """ + if linked: + return self._linked_external_resources + return self._external_resources @property def external_resources(self): """Return the HERD external resources object for this NWBFile. - If an external HERD has been linked via ``link_resources``, that object - is returned. Otherwise, the internal HERD set via ``__init__`` or the - setter is returned. + If a HERD has been linked via ``link_resources``, that object is + returned. Otherwise, the HERD set via ``__init__`` or the setter is + returned. Use ``get_external_resources`` to access a specific one. """ - if self._external_herd is not None: - return self._external_herd - return self._internal_herd + if self._linked_external_resources is not None: + return self._linked_external_resources + return self._external_resources @external_resources.setter - def external_resources(self, herd): - """Set the internal HERD external resources object for this NWBFile.""" - self._internal_herd = herd - self._internal_herd.parent = self + def external_resources(self, external_resources): + """Set the HERD external resources object for this NWBFile.""" + self._external_resources = external_resources + self._external_resources.parent = self @property def objects(self): diff --git a/test_herd_access.py b/test_herd_access.py new file mode 100644 index 000000000..894341c3a --- /dev/null +++ b/test_herd_access.py @@ -0,0 +1,47 @@ +"""Test how external_resources can be accessed on NWBFile.""" +from datetime import datetime +from uuid import uuid4 + +from dateutil import tz + +from pynwb import NWBFile +from pynwb.resources import HERD + +session_start_time = datetime(2018, 4, 25, 2, 30, 3, tzinfo=tz.gettz("US/Pacific")) +herd = HERD() +nwbfile = NWBFile( + session_description="test", + identifier=str(uuid4()), + session_start_time=session_start_time, + external_resources=herd, +) + +# Try various ways to access external_resources +attrs_to_try = [ + "external_resources", + "_external_resources", + "_linked_external_resources", + "general_external_resources", + "general__external_resources", + "fields", +] + +for attr in attrs_to_try: + val = getattr(nwbfile, attr, "NOT FOUND") + print(f"nwbfile.{attr}: {val}") + +print() + +# Check if external_resources shows up in fields +print("nwbfile.fields:", nwbfile.fields) +print() + +# Check children +for child in nwbfile.children: + if "external" in type(child).__name__.lower() or "herd" in type(child).__name__.lower(): + print(f"Found in children: {child} (type={type(child).__name__})") + +# Check if the HERD is the same object +print() +print(f"nwbfile.external_resources is herd: {nwbfile.external_resources is herd}") +print(f"nwbfile._external_resources is herd: {nwbfile._external_resources is herd}") diff --git a/tests/unit/test_resources.py b/tests/unit/test_resources.py index 6dceb5050..cf271dcb0 100644 --- a/tests/unit/test_resources.py +++ b/tests/unit/test_resources.py @@ -125,8 +125,21 @@ def test_resources_roundtrip(self): ), ) + def test_get_external_resources(self): + """Test get_external_resources returns the correct HERD based on the linked parameter.""" + nwbfile, subject = self._create_nwbfile_with_herd() + original_herd = nwbfile.external_resources + + linked_herd = HERD() + nwbfile.link_resources(linked_herd) + + self.assertIs(nwbfile.get_external_resources(linked=False), original_herd) + self.assertIs(nwbfile.get_external_resources(linked=True), linked_herd) + # property returns linked when available + self.assertIs(nwbfile.external_resources, linked_herd) + def test_link_resources(self): - """Make sure that the internal HERD is not overwritten on export.""" + """Make sure that the original HERD is not overwritten on export.""" nwbfile, subject = self._create_nwbfile_with_herd() with NWBHDF5IO(self.path, "w") as io: From d87b44f1c34a3459d707305e9d5cd4ad224f51bf Mon Sep 17 00:00:00 2001 From: rly <310197+rly@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:19:31 -0700 Subject: [PATCH 17/20] Remove test_herd_access.py scratch file Co-Authored-By: Claude Opus 4.6 --- test_herd_access.py | 47 --------------------------------------------- 1 file changed, 47 deletions(-) delete mode 100644 test_herd_access.py diff --git a/test_herd_access.py b/test_herd_access.py deleted file mode 100644 index 894341c3a..000000000 --- a/test_herd_access.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Test how external_resources can be accessed on NWBFile.""" -from datetime import datetime -from uuid import uuid4 - -from dateutil import tz - -from pynwb import NWBFile -from pynwb.resources import HERD - -session_start_time = datetime(2018, 4, 25, 2, 30, 3, tzinfo=tz.gettz("US/Pacific")) -herd = HERD() -nwbfile = NWBFile( - session_description="test", - identifier=str(uuid4()), - session_start_time=session_start_time, - external_resources=herd, -) - -# Try various ways to access external_resources -attrs_to_try = [ - "external_resources", - "_external_resources", - "_linked_external_resources", - "general_external_resources", - "general__external_resources", - "fields", -] - -for attr in attrs_to_try: - val = getattr(nwbfile, attr, "NOT FOUND") - print(f"nwbfile.{attr}: {val}") - -print() - -# Check if external_resources shows up in fields -print("nwbfile.fields:", nwbfile.fields) -print() - -# Check children -for child in nwbfile.children: - if "external" in type(child).__name__.lower() or "herd" in type(child).__name__.lower(): - print(f"Found in children: {child} (type={type(child).__name__})") - -# Check if the HERD is the same object -print() -print(f"nwbfile.external_resources is herd: {nwbfile.external_resources is herd}") -print(f"nwbfile._external_resources is herd: {nwbfile._external_resources is herd}") From bbdf5ba361d2cd5049bd910b66b1061316cbcd24 Mon Sep 17 00:00:00 2001 From: rly <310197+rly@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:53:50 -0700 Subject: [PATCH 18/20] Use HERDManager abstract interface for external_resources Remove custom external_resources property/setter on NWBFile and rely on the auto-generated property from __nwbfields__. The HERDManager mixin is now an abstract interface in hdmf, and declaring external_resources in __nwbfields__ with child=True satisfies it. The external_resources attribute now always returns the owned HERD. Use get_external_resources(linked=True) to access a linked HERD. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/pynwb/file.py | 37 +++++++----------------------------- tests/unit/test_resources.py | 11 ++++++----- 2 files changed, 13 insertions(+), 35 deletions(-) diff --git a/src/pynwb/file.py b/src/pynwb/file.py index 3afe85e20..06b603742 100644 --- a/src/pynwb/file.py +++ b/src/pynwb/file.py @@ -483,19 +483,14 @@ def __init__(self, **kwargs): 'icephys_simultaneous_recordings', 'icephys_sequential_recordings', 'icephys_repetitions', - 'icephys_experimental_conditions' + 'icephys_experimental_conditions', + 'external_resources' ] args_to_set = popargs_to_dict(keys_to_set, kwargs) - external_resources = popargs('external_resources', kwargs) kwargs['name'] = 'root' super().__init__(**kwargs) self._linked_external_resources = None - self._external_resources = None - - if external_resources is not None: - # NOTE: this sets self._external_resources via the property setter - self.external_resources = external_resources # add timezone to session_start_time if missing session_start_time = args_to_set['session_start_time'] @@ -584,9 +579,9 @@ def all_children(self): def link_resources(self, external_resources): """Link an external HERD object as the external resources for this file. - The linked HERD will be returned by the ``external_resources`` property - but will not be written on export; the original HERD (if any) is - preserved in the exported file. + The linked HERD will not be written on export; the original HERD + (if any) is preserved in the exported file. Use + ``get_external_resources(linked=True)`` to access the linked HERD. """ self._linked_external_resources = external_resources @@ -598,29 +593,11 @@ def get_external_resources(self, linked=False): linked : bool, optional If True, return the linked HERD set via ``link_resources``. If False (default), return the HERD set via ``__init__`` or the - ``external_resources`` setter. + ``external_resources`` attribute. """ if linked: return self._linked_external_resources - return self._external_resources - - @property - def external_resources(self): - """Return the HERD external resources object for this NWBFile. - - If a HERD has been linked via ``link_resources``, that object is - returned. Otherwise, the HERD set via ``__init__`` or the setter is - returned. Use ``get_external_resources`` to access a specific one. - """ - if self._linked_external_resources is not None: - return self._linked_external_resources - return self._external_resources - - @external_resources.setter - def external_resources(self, external_resources): - """Set the HERD external resources object for this NWBFile.""" - self._external_resources = external_resources - self._external_resources.parent = self + return self.external_resources @property def objects(self): diff --git a/tests/unit/test_resources.py b/tests/unit/test_resources.py index cf271dcb0..3e340c906 100644 --- a/tests/unit/test_resources.py +++ b/tests/unit/test_resources.py @@ -135,8 +135,8 @@ def test_get_external_resources(self): self.assertIs(nwbfile.get_external_resources(linked=False), original_herd) self.assertIs(nwbfile.get_external_resources(linked=True), linked_herd) - # property returns linked when available - self.assertIs(nwbfile.external_resources, linked_herd) + # attribute returns the original, not the linked one + self.assertIs(nwbfile.external_resources, original_herd) def test_link_resources(self): """Make sure that the original HERD is not overwritten on export.""" @@ -149,9 +149,10 @@ def test_link_resources(self): read_nwbfile = read_io.read() read_nwbfile.link_resources(HERD()) - self.assertEqual(read_nwbfile.external_resources.keys.data, []) - self.assertEqual(read_nwbfile.external_resources.entities.data, []) - self.assertEqual(read_nwbfile.external_resources.objects.data, []) + linked = read_nwbfile.get_external_resources(linked=True) + self.assertEqual(linked.keys.data, []) + self.assertEqual(linked.entities.data, []) + self.assertEqual(linked.objects.data, []) with NWBHDF5IO(self.export_path, mode="w") as export_io: export_io.export(src_io=read_io, nwbfile=read_nwbfile) From 15da4c7ebefcdaedadb5fcab88e123e4656ed00e Mon Sep 17 00:00:00 2001 From: rly <310197+rly@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:33:13 -0700 Subject: [PATCH 19/20] Use HERDManager.link_resources and get_external_resources from hdmf Remove link_resources and get_external_resources from NWBFile since these methods now live on the HERDManager base class in hdmf. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 2 +- src/pynwb/file.py | 25 ------------------------- 2 files changed, 1 insertion(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d70e6b340..5183e012a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - Fixed broken and redirecting links in documentation. @bendichter [#2165](https://github.com/NeurodataWithoutBorders/pynwb/pull/2165) ### Added -- Added support for HERD (HDMF External Resources Data Structure) as the `external_resources` field on `NWBFile`, enabling users to associate external resource annotations (e.g., ontology term mappings) with their NWB files. Also added `NWBFile.link_resources` for linking an external HERD object. @mavaylon1, @rly [#2111](https://github.com/NeurodataWithoutBorders/pynwb/pull/2111) +- Added support for HERD (HDMF External Resources Data Structure) as the `external_resources` field on `NWBFile`, enabling users to associate external resource annotations (e.g., ontology term mappings) with their NWB files. `link_resources` and `get_external_resources` are inherited from `HERDManager` in hdmf. @mavaylon1, @rly [#2111](https://github.com/NeurodataWithoutBorders/pynwb/pull/2111) - Added `get_starting_time()` and `get_duration()` methods to `TimeSeries` to get the starting time and duration of the time series. @h-mayorquin [#2146](https://github.com/NeurodataWithoutBorders/pynwb/pull/2146) - Added `get_starting_time()` and `get_duration()` methods to `TimeIntervals` to get the earliest start time and total duration (span from earliest start to latest stop) of all intervals. @h-mayorquin [#2146](https://github.com/NeurodataWithoutBorders/pynwb/pull/2146) - Added `get_starting_time()` and `get_duration()` methods to `Units` to get the earliest spike time and total duration (span from earliest to latest spike) across all units. @h-mayorquin [#2164](https://github.com/NeurodataWithoutBorders/pynwb/pull/2164) diff --git a/src/pynwb/file.py b/src/pynwb/file.py index 06b603742..4def8634b 100644 --- a/src/pynwb/file.py +++ b/src/pynwb/file.py @@ -490,8 +490,6 @@ def __init__(self, **kwargs): kwargs['name'] = 'root' super().__init__(**kwargs) - self._linked_external_resources = None - # add timezone to session_start_time if missing session_start_time = args_to_set['session_start_time'] if session_start_time.tzinfo is None: @@ -576,29 +574,6 @@ def all_children(self): stack.append(c) return ret - def link_resources(self, external_resources): - """Link an external HERD object as the external resources for this file. - - The linked HERD will not be written on export; the original HERD - (if any) is preserved in the exported file. Use - ``get_external_resources(linked=True)`` to access the linked HERD. - """ - self._linked_external_resources = external_resources - - def get_external_resources(self, linked=False): - """Return the HERD external resources object for this NWBFile. - - Parameters - ---------- - linked : bool, optional - If True, return the linked HERD set via ``link_resources``. - If False (default), return the HERD set via ``__init__`` or the - ``external_resources`` attribute. - """ - if linked: - return self._linked_external_resources - return self.external_resources - @property def objects(self): if self.__obj is None: From cba3474788452df6d4065b062c7fefa798892791 Mon Sep 17 00:00:00 2001 From: rly <310197+rly@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:17:00 -0700 Subject: [PATCH 20/20] Update hdmf minimum version to 5.1.0 Co-Authored-By: Claude Opus 4.6 (1M context) --- environment-ros3.yml | 2 +- pyproject.toml | 2 +- requirements-min.txt | 2 +- requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/environment-ros3.yml b/environment-ros3.yml index 80fd3cd51..657af85a1 100644 --- a/environment-ros3.yml +++ b/environment-ros3.yml @@ -6,7 +6,7 @@ channels: dependencies: - python==3.14 - h5py==3.15.1 - - hdmf==5.0.1 + - hdmf==5.1.0 - matplotlib==3.10.8 - numpy==2.4.2 - pandas==2.3.3 diff --git a/pyproject.toml b/pyproject.toml index 4c613f3e9..686df0e2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ classifiers = [ ] dependencies = [ "h5py>=3.6.0", - "hdmf>=5.0.1,<6", + "hdmf>=5.1.0,<6", "numpy>=1.24.0", "pandas>=1.4.0,<3", "python-dateutil>=2.8.2", diff --git a/requirements-min.txt b/requirements-min.txt index 5960d377d..1ce9681cf 100644 --- a/requirements-min.txt +++ b/requirements-min.txt @@ -1,6 +1,6 @@ # minimum versions of package dependencies for installing PyNWB h5py==3.6.0 -hdmf==5.0.1 +hdmf==5.1.0 numpy==1.24.0 pandas==1.4.0 python-dateutil==2.8.2 diff --git a/requirements.txt b/requirements.txt index c73e0cf46..494792564 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # pinned dependencies to reproduce an entire development environment to use PyNWB h5py==3.16.0 -hdmf==5.0.1 +hdmf==5.1.0 numpy==2.4.3 pandas==2.3.3 python-dateutil==2.9.0.post0