From 4e9492f6dd0611ac4a72913dfe08809d47bb67a6 Mon Sep 17 00:00:00 2001 From: NicolasGensollen Date: Wed, 7 Aug 2024 15:50:46 +0200 Subject: [PATCH] provide more flexibility for comparing different versions of the specs --- clinica/utils/caps.py | 77 +++++++++++++++++++++++++++---- test/unittests/utils/test_caps.py | 23 +++++++++ 2 files changed, 92 insertions(+), 8 deletions(-) diff --git a/clinica/utils/caps.py b/clinica/utils/caps.py index b37b39310..a1acd5aac 100644 --- a/clinica/utils/caps.py +++ b/clinica/utils/caps.py @@ -1,7 +1,8 @@ import json from datetime import datetime +from enum import Enum from pathlib import Path -from typing import IO, List, MutableSequence, NewType, Optional +from typing import IO, List, MutableSequence, NewType, Optional, Union from attrs import define, fields from cattr.gen import make_dict_structure_fn, make_dict_unstructure_fn, override @@ -17,6 +18,8 @@ __all__ = [ "CAPS_VERSION", "CAPSDatasetDescription", + "VersionComparisonPolicy", + "are_versions_compatible", "write_caps_dataset_description", "build_caps_dataset_description", ] @@ -27,6 +30,60 @@ IsoDate = NewType("IsoDate", datetime) +class VersionComparisonPolicy(str, Enum): + """Defines the different ways we can compare version numbers in Clinica. + + STRICT: version numbers have to match exactly. + MINOR : version numbers have to have the same major and minor numbers. + MAJOR: version numbers only need to share the same major number. + """ + + STRICT = "strict" + MINOR = "minor" + MAJOR = "major" + + +def are_versions_compatible( + version1: Union[str, Version], + version2: Union[str, Version], + policy: Optional[Union[str, VersionComparisonPolicy]] = None, +) -> bool: + """Returns whether the two provided versions are compatible or not depending on the policy. + + Parameters + ---------- + version1 : str or Version + The first version number to compare. + + version2 : str or Version + The second version number to compare. + + policy : str or VersionComparisonPolicy, optional + The policy under which to compare version1 with version2. + By default, a strict policy is used, meaning that version + numbers have to match exactly. + + Returns + ------- + bool : + True if version1 is 'compatible' with version2, False otherwise. + """ + if isinstance(version1, str): + version1 = Version(version1) + if isinstance(version2, str): + version2 = Version(version2) + if policy is None: + policy = VersionComparisonPolicy.STRICT + else: + policy = VersionComparisonPolicy(policy) + if policy == VersionComparisonPolicy.STRICT: + return version1 == version2 + if policy == VersionComparisonPolicy.MINOR: + return version1.major == version2.major and version1.minor == version2.minor + if policy == VersionComparisonPolicy.MAJOR: + return version1.major == version2.major + + @define class CAPSProcessingDescription: """This class models a CAPS processing pipeline metadata. @@ -209,12 +266,14 @@ def from_dict(cls, values: dict): processing, ) - def is_compatible_with(self, other) -> bool: - if self.bids_version != other.bids_version: - return False - if self.caps_version != other.caps_version: - return False - return True + def is_compatible_with( + self, other, policy: Optional[Union[str, VersionComparisonPolicy]] = None + ) -> bool: + return are_versions_compatible( + self.bids_version, other.bids_version, policy=policy + ) and are_versions_compatible( + self.caps_version, other.caps_version, policy=policy + ) def _get_username() -> str: @@ -452,7 +511,9 @@ def build_caps_dataset_description( previous_desc = CAPSDatasetDescription.from_file( output_dir / "dataset_description.json" ) - if not previous_desc.is_compatible_with(new_desc): + if not previous_desc.is_compatible_with( + new_desc, VersionComparisonPolicy.STRICT + ): msg = ( f"Impossible to write the 'dataset_description.json' file in {output_dir} " "because it already exists and it contains incompatible metadata." diff --git a/test/unittests/utils/test_caps.py b/test/unittests/utils/test_caps.py index 75276a880..38332d771 100644 --- a/test/unittests/utils/test_caps.py +++ b/test/unittests/utils/test_caps.py @@ -346,3 +346,26 @@ def test_write_caps_dataset_description_multiple_processing(tmp_path, mocker): }, ], } + + +@pytest.mark.parametrize( + "version1,version2,policy,expected", + [ + ("1.2.3", "1.2.3", "strict", True), + ("1.2.3", "1.2.3", "minor", True), + ("1.2.3", "1.2.3", "major", True), + ("1.2.3", "1.2.4", "strict", False), + ("1.2.3", "1.2.4", "minor", True), + ("1.2.3", "1.2.4", "major", True), + ("1.2.3", "1.3.3", "strict", False), + ("1.2.3", "1.3.3", "minor", False), + ("1.2.3", "1.3.3", "major", True), + ("1.2.3", "2.2.3", "strict", False), + ("1.2.3", "2.2.3", "minor", False), + ("1.2.3", "2.2.3", "major", False), + ], +) +def test_are_versions_compatible(version1, version2, policy, expected): + from clinica.utils.caps import are_versions_compatible + + assert are_versions_compatible(version1, version2, policy=policy) is expected