diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cc2419..607fa27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ - Set `num_samples` on the external `ImageSeries` objects used in the mocks, tests, and examples. pynwb 4.0 requires `num_samples` when `format='external'` and timing is specified with `rate`, because the empty data array cannot be used to infer the number of frames. @rly (#62) +- `PoseEstimation` now raises when `source_software_version` is set without `source_software`. The version is + stored as an attribute on the `source_software` dataset, so it was previously dropped silently on roundtrip. + @h-mayorquin (#63) ## ndx-pose 0.3.0 (June 2, 2026) diff --git a/src/pynwb/ndx_pose/pose.py b/src/pynwb/ndx_pose/pose.py index 917f0be..ef5ba46 100644 --- a/src/pynwb/ndx_pose/pose.py +++ b/src/pynwb/ndx_pose/pose.py @@ -319,6 +319,11 @@ def __init__(self, **kwargs): pose_estimation_series, description = popargs("pose_estimation_series", "description", kwargs) scorer = popargs("scorer", kwargs) source_software, source_software_version = popargs("source_software", "source_software_version", kwargs) + if source_software_version is not None and source_software is None: + raise ValueError( + "'source_software_version' was specified without 'source_software'. The version is stored as an " + "attribute on the 'source_software' dataset, so 'source_software' must be provided as well." + ) super().__init__(**kwargs) self.pose_estimation_series = pose_estimation_series diff --git a/src/pynwb/tests/unit/test_pose.py b/src/pynwb/tests/unit/test_pose.py index bb82f27..a75abf8 100644 --- a/src/pynwb/tests/unit/test_pose.py +++ b/src/pynwb/tests/unit/test_pose.py @@ -182,6 +182,27 @@ def test_bad_device_link(self): skeleton=skeleton, ) + def test_source_software_version_without_source_software_raises(self): + """Test that setting source_software_version without source_software raises. + + source_software_version is stored as the 'version' attribute on the 'source_software' + dataset, so it cannot be written without source_software and would otherwise be silently + dropped on roundtrip. + """ + skeleton = mock_Skeleton() + pose_estimation_series = [mock_PoseEstimationSeries(name=name) for name in skeleton.nodes] + + msg = ( + "'source_software_version' was specified without 'source_software'. The version is stored as an " + "attribute on the 'source_software' dataset, so 'source_software' must be provided as well." + ) + with self.assertRaisesWith(ValueError, msg): + PoseEstimation( + pose_estimation_series=pose_estimation_series, + skeleton=skeleton, + source_software_version="2.2b8", + ) + def test_constructor_nodes_edges(self): """Test the old constructor for PoseEstimation with nodes and edges.""" front_left_paw = mock_PoseEstimationSeries(