diff --git a/src/nwbinspector/_internal_configs/dandi.inspector_config.yaml b/src/nwbinspector/_internal_configs/dandi.inspector_config.yaml index 9a79c8e3..240dfab8 100644 --- a/src/nwbinspector/_internal_configs/dandi.inspector_config.yaml +++ b/src/nwbinspector/_internal_configs/dandi.inspector_config.yaml @@ -8,5 +8,6 @@ CRITICAL: # All the fields under CRITICAL will be required for dandi validate t - check_subject_age - check_subject_proper_age_range - check_session_id_no_slashes + - check_nwb_schema_version_official_release BEST_PRACTICE_VIOLATION: - check_data_orientation # not 100% accurate, so need to deelevate from CRITICAL to skip it in dandi validate diff --git a/src/nwbinspector/checks/__init__.py b/src/nwbinspector/checks/__init__.py index cf231978..a4006af4 100644 --- a/src/nwbinspector/checks/__init__.py +++ b/src/nwbinspector/checks/__init__.py @@ -41,6 +41,7 @@ check_experimenter_form, check_institution, check_keywords, + check_nwb_schema_version_official_release, check_processing_module_name, check_session_id_no_slashes, check_session_start_time_future_date, @@ -108,6 +109,7 @@ "check_large_dataset_compression", "check_keywords", "check_institution", + "check_nwb_schema_version_official_release", "check_subject_age", "check_subject_sex", "check_subject_exists", diff --git a/src/nwbinspector/checks/_nwbfile_metadata.py b/src/nwbinspector/checks/_nwbfile_metadata.py index 1321e812..1831320a 100644 --- a/src/nwbinspector/checks/_nwbfile_metadata.py +++ b/src/nwbinspector/checks/_nwbfile_metadata.py @@ -20,6 +20,54 @@ PROCESSING_MODULE_CONFIG = ["ophys", "ecephys", "icephys", "behavior", "misc", "ogen", "retinotopy"] +@register_check(importance=Importance.BEST_PRACTICE_VIOLATION, neurodata_type=NWBFile) +def check_nwb_schema_version_official_release(nwbfile: NWBFile) -> Optional[InspectorMessage]: + """ + Check if the NWB schema version used is an official release version. + + Unofficial versions include development versions (e.g., '2.8.0-dev'), + pre-release versions (e.g., '2.8.0-alpha', '2.8.0-beta', '2.8.0-rc1'), + or any version containing non-standard suffixes. + + Best Practice: Use only official, released versions of the NWB schema for + production data to ensure compatibility and reproducibility. + """ + try: + import pynwb + + # Get the schema version from the namespace catalog + manager = pynwb.get_manager() + core_namespace = manager.namespace_catalog.get_namespace("core") + schema_version = core_namespace.version + + # Define pattern for official release versions (semantic versioning) + # Official versions should match pattern: MAJOR.MINOR.PATCH (e.g., "2.8.0") + official_version_pattern = r"^\d+\.\d+\.\d+$" + + # Check if the version matches the official pattern + if not re.match(official_version_pattern, schema_version): + # Define common unofficial version patterns for better error messages + if re.match(r"^\d+\.\d+\.\d+-(dev|alpha|beta|rc\d*|pre).*$", schema_version): + version_type = "development or pre-release" + else: + version_type = "non-standard" + + return InspectorMessage( + message=( + f"The NWB schema version '{schema_version}' appears to be a {version_type} version. " + f"For production data, it is recommended to use only official release versions " + f"(e.g., '2.8.0') to ensure compatibility and reproducibility." + ) + ) + + except Exception as exception: + # If we can't determine the schema version, don't report an error + # This could happen with very old versions of PyNWB or in unusual setups + return None + + return None + + @register_check(importance=Importance.BEST_PRACTICE_SUGGESTION, neurodata_type=NWBFile) def check_session_start_time_old_date(nwbfile: NWBFile) -> Optional[InspectorMessage]: """ diff --git a/tests/test_check_configuration.py b/tests/test_check_configuration.py index c6078086..302bdd53 100644 --- a/tests/test_check_configuration.py +++ b/tests/test_check_configuration.py @@ -106,6 +106,7 @@ def test_load_config(self): "check_subject_age", "check_subject_proper_age_range", "check_session_id_no_slashes", + "check_nwb_schema_version_official_release", ], BEST_PRACTICE_VIOLATION=[ "check_data_orientation", diff --git a/tests/unit_tests/test_nwbfile_metadata.py b/tests/unit_tests/test_nwbfile_metadata.py index 216cbd26..74ef8210 100644 --- a/tests/unit_tests/test_nwbfile_metadata.py +++ b/tests/unit_tests/test_nwbfile_metadata.py @@ -12,6 +12,7 @@ check_experimenter_form, check_institution, check_keywords, + check_nwb_schema_version_official_release, check_processing_module_name, check_session_id_no_slashes, check_session_start_time_future_date, @@ -619,3 +620,75 @@ def test_check_subject_id_with_slashes(): object_name="subject", location="/general/subject", ) + + +def test_check_nwb_schema_version_official_release_pass(): + """Test that official schema versions pass the check.""" + # The current schema version (2.8.0) should be considered official + assert check_nwb_schema_version_official_release(minimal_nwbfile) is None + + +def test_check_nwb_schema_version_official_release_fail(): + """Test that unofficial schema versions fail the check.""" + # We need to mock the schema version to test different scenarios + import unittest.mock + + # Test development version + with unittest.mock.patch("pynwb.get_manager") as mock_manager: + mock_catalog = unittest.mock.MagicMock() + mock_namespace = unittest.mock.MagicMock() + mock_namespace.version = "2.8.0-dev" + mock_catalog.get_namespace.return_value = mock_namespace + mock_manager.return_value.namespace_catalog = mock_catalog + + result = check_nwb_schema_version_official_release(minimal_nwbfile) + assert result == InspectorMessage( + message=( + "The NWB schema version '2.8.0-dev' appears to be a development or pre-release version. " + "For production data, it is recommended to use only official release versions " + "(e.g., '2.8.0') to ensure compatibility and reproducibility." + ), + importance=Importance.BEST_PRACTICE_VIOLATION, + check_function_name="check_nwb_schema_version_official_release", + object_type="NWBFile", + object_name="root", + location="/", + ) + + +def test_check_nwb_schema_version_pre_release_versions(): + """Test that various pre-release versions are detected.""" + import unittest.mock + + test_cases = [ + ("2.8.0-alpha", "development or pre-release"), + ("2.8.0-beta", "development or pre-release"), + ("2.8.0-rc1", "development or pre-release"), + ("2.8.0-dev", "development or pre-release"), + ("2.8.0-pre", "development or pre-release"), + ("2.8.0.custom", "non-standard"), + ("v2.8.0", "non-standard"), + ] + + for version, version_type in test_cases: + with unittest.mock.patch("pynwb.get_manager") as mock_manager: + mock_catalog = unittest.mock.MagicMock() + mock_namespace = unittest.mock.MagicMock() + mock_namespace.version = version + mock_catalog.get_namespace.return_value = mock_namespace + mock_manager.return_value.namespace_catalog = mock_catalog + + result = check_nwb_schema_version_official_release(minimal_nwbfile) + assert result is not None + assert version in result.message + assert version_type in result.message + + +def test_check_nwb_schema_version_exception_handling(): + """Test that exceptions in schema version detection are handled gracefully.""" + import unittest.mock + + with unittest.mock.patch("pynwb.get_manager", side_effect=Exception("Mock error")): + # Should return None when unable to determine schema version + result = check_nwb_schema_version_official_release(minimal_nwbfile) + assert result is None