diff --git a/CHANGELOG.md b/CHANGELOG.md index 52f69b02b..ce3b3dff0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed - Fixed `Units.waveform_unit` having no effect on the written file. The `waveform_unit` passed to `Units` is now written to the `waveform_mean`, `waveform_sd`, and `waveforms` columns, and `"volts"` remains the default. PyNWB now also warns when the waveform columns of a file being read carry different `unit` or `sampling_rate` attributes, since only one value per attribute is kept on the `Units` container. @rly [#2162](https://github.com/NeurodataWithoutBorders/pynwb/issues/2162) +- Fixed `NWBFile.objects` being cached on first access and never invalidated, so containers added to (or removed from) the file after the property was first read were missing from (or still in) it. It is now rebuilt on every access and always reflects the current contents of the file. @HugoFara [#2242](https://github.com/NeurodataWithoutBorders/pynwb/issues/2242) - Fixed `mock_DeviceModel` defaulting `manufacturer` to `None`. The mock now defaults it to `"manufacturer"`. @HugoFara [#2232](https://github.com/NeurodataWithoutBorders/pynwb/pull/2232) - Fixed reading a file whose dates carry a sub-minute UTC offset (e.g. `1900-10-01T00:00:00-05:50:36`). @h-mayorquin [#2230](https://github.com/NeurodataWithoutBorders/pynwb/pull/2230) - Fixed wide pandas DataFrames in the tutorials spilling out of the content column and into the right margin. @bendichter [#2236](https://github.com/NeurodataWithoutBorders/pynwb/pull/2236) diff --git a/src/pynwb/file.py b/src/pynwb/file.py index a2aa9d4ed..841a7a634 100644 --- a/src/pynwb/file.py +++ b/src/pynwb/file.py @@ -566,18 +566,14 @@ def __init__(self, **kwargs): for key, val in args_to_set.items(): setattr(self, key, val) - self.__obj = None - def all_children(self): + """Get a list of this file and all of its children, recursively.""" stack = [self] ret = list() - self.__obj = LabelledDict(label='all_objects', key_attr='object_id') while len(stack): n = stack.pop() ret.append(n) - if n.object_id is not None: - self.__obj[n.object_id] = n - else: + if n.object_id is None: warn('%s "%s" does not have an object_id' % (n.neurodata_type, n.name)) if hasattr(n, 'children'): for c in n.children: @@ -586,9 +582,15 @@ def all_children(self): @property def objects(self): - if self.__obj is None: - self.all_children() - return self.__obj + """A dict of this file and all of its children, recursively, keyed by object ID. + + The dict is rebuilt on every access, so it always reflects the current contents of the file. + """ + ret = LabelledDict(label='all_objects', key_attr='object_id') + for n in self.all_children(): + if n.object_id is not None: + ret[n.object_id] = n + return ret @property def epoch_tags(self): diff --git a/tests/unit/test_file.py b/tests/unit/test_file.py index 93f099022..3dbaac3fe 100644 --- a/tests/unit/test_file.py +++ b/tests/unit/test_file.py @@ -444,6 +444,30 @@ def test_all_children(self): self.assertIn(device, children) self.assertIn(elecgrp, children) + def test_objects_includes_container_added_after_first_access(self): + """Reading objects must not freeze its contents: containers added later must show up.""" + self.assertNotIn('test_ts', [obj.name for obj in self.nwbfile.objects.values()]) + ts = TimeSeries(name='test_ts', data=[0, 1, 2], unit='grams', timestamps=[0.0, 0.1, 0.2]) + self.nwbfile.add_acquisition(ts) + self.assertIs(self.nwbfile.objects[ts.object_id], ts) + + def test_objects_includes_nested_container_added_after_first_access(self): + """Containers added to a child of the file must show up in objects as well.""" + module = self.nwbfile.create_processing_module(name='behavior', description='a test module') + self.nwbfile.objects + ts = TimeSeries(name='test_ts', data=[0, 1, 2], unit='grams', timestamps=[0.0, 0.1, 0.2]) + module.add(ts) + self.assertIs(self.nwbfile.objects[ts.object_id], ts) + + def test_objects_excludes_removed_container(self): + """Containers removed from the file must not linger in objects.""" + ts = TimeSeries(name='test_ts', data=[0, 1, 2], unit='grams', timestamps=[0.0, 0.1, 0.2]) + self.nwbfile.add_acquisition(ts) + self.assertIn(ts.object_id, self.nwbfile.objects) + del self.nwbfile.acquisition['test_ts'] + ts.reset_parent() + self.assertNotIn(ts.object_id, self.nwbfile.objects) + def test_fail_if_source_script_file_name_without_source_script(self): with self.assertRaises(ValueError): # <-- source_script_file_name without source_script is not allowed