diff --git a/bauh/gems/arch/dependencies.py b/bauh/gems/arch/dependencies.py index 67f3c410..a8af0530 100644 --- a/bauh/gems/arch/dependencies.py +++ b/bauh/gems/arch/dependencies.py @@ -7,7 +7,7 @@ from bauh.api.abstract.handler import ProcessWatcher from bauh.gems.arch import pacman, message, sorting, confirmation from bauh.gems.arch.aur import AURClient from bauh.gems.arch.exceptions import PackageNotFoundException -from bauh.gems.arch.version import compare_versions +from bauh.gems.arch.version import match_required_version from bauh.view.util.translation import I18n @@ -229,7 +229,7 @@ class DependenciesAnalyser: if len(split_dep) == 3 and split_dep[0] == dep_name: version_provided = split_dep[2] - if compare_versions(version_provided, exp_op, version_required): + if match_required_version(version_provided, exp_op, version_required): matched_providers.add(p) break @@ -275,9 +275,9 @@ class DependenciesAnalyser: split_informed_dep = self.re_dep_operator.split(dep_exp) try: version_required = split_informed_dep[2] - exp_op = split_informed_dep[1] if split_informed_dep[1] != '=' else '==' + exp_op = split_informed_dep[1].strip() - if compare_versions(dep_version, exp_op, version_required): + if match_required_version(dep_version, exp_op, version_required): aur_deps.add(dep_name) missing_deps.add((dep_name, 'aur')) except: @@ -333,9 +333,9 @@ class DependenciesAnalyser: if version_found: version_found = version_found[0].split('=')[1] version_required = dep_split[2] - op = dep_split[1] if dep_split[1] != '=' else '==' + op = dep_split[1].strip() - if not compare_versions(version_found, op, version_required): + if not match_required_version(version_found, op, version_required): self._fill_missing_dep(dep_name=dep_name, dep_exp=dep, aur_index=aur_index, missing_deps=missing_deps, remote_provided_map=remote_provided_map, diff --git a/bauh/gems/arch/mapper.py b/bauh/gems/arch/mapper.py index 30b11425..ce1cd365 100644 --- a/bauh/gems/arch/mapper.py +++ b/bauh/gems/arch/mapper.py @@ -5,7 +5,7 @@ from typing import Optional from bauh.api.abstract.model import PackageStatus from bauh.api.http import HttpClient from bauh.gems.arch.model import ArchPackage -from bauh.gems.arch.version import compare_versions +from bauh.gems.arch.version import normalize_version from bauh.view.util.translation import I18n URL_PKG_DOWNLOAD = 'https://aur.archlinux.org/{}' @@ -61,7 +61,7 @@ class AURDataMapper: @staticmethod def check_version_update(version: str, latest_version: str) -> bool: if version and latest_version and version != latest_version: - return compare_versions(latest_version, '>', version) + return normalize_version(latest_version) > normalize_version(version) return False diff --git a/bauh/gems/arch/updates.py b/bauh/gems/arch/updates.py index 7a62eafb..032af4f5 100644 --- a/bauh/gems/arch/updates.py +++ b/bauh/gems/arch/updates.py @@ -12,7 +12,7 @@ from bauh.gems.arch.dependencies import DependenciesAnalyser from bauh.gems.arch.exceptions import PackageNotFoundException from bauh.gems.arch.model import ArchPackage from bauh.gems.arch.pacman import RE_DEP_OPERATORS -from bauh.gems.arch.version import compare_versions +from bauh.gems.arch.version import match_required_version from bauh.view.util.translation import I18n @@ -725,14 +725,11 @@ class UpdatesSummarizer: if versions: op = ''.join(RE_DEP_OPERATORS.findall(dep)) - if op == '=': - op = '==' - version_match = False for v in versions: try: - if compare_versions(v, op, dep_split[1]): + if match_required_version(current_version=v, operator=op, required_version=dep_split[1]): version_match = True break except: diff --git a/bauh/gems/arch/version.py b/bauh/gems/arch/version.py index cf4574d4..1f55cc9c 100644 --- a/bauh/gems/arch/version.py +++ b/bauh/gems/arch/version.py @@ -1,35 +1,51 @@ -from typing import Optional +import re -from packaging.version import parse +from packaging.version import LegacyVersion + +RE_VERSION_WITH_RELEASE = re.compile(r'^(.+)-\d+$') +RE_VERSION_WITH_EPOCH = re.compile(r'^\d+:(.+)$') -def normalize_version(version: str) -> Optional[str]: - if version: - final_version = version.strip() +def normalize_version(version: str) -> LegacyVersion: + final_version = version.strip() - if final_version: - if ':' not in final_version: - final_version = '0:{}'.format(final_version) + if not RE_VERSION_WITH_EPOCH.match(final_version): + final_version = '0:{}'.format(final_version) - if '-' not in final_version: - final_version = '{}-1'.format(final_version) + if not RE_VERSION_WITH_RELEASE.match(final_version): + final_version = '{}-1'.format(final_version) - return final_version + return LegacyVersion(final_version) -def compare_versions(version_a: str, operator: str, version_b: str) -> bool: - if operator: - a_obj, b_obj = parse(normalize_version(version_a)), parse(normalize_version(version_b)) +def match_required_version(current_version: str, operator: str, required_version: str) -> bool: + final_required, final_current = required_version.strip(), current_version.strip() - if operator == '>': - return a_obj > b_obj - elif operator == '>=': - return a_obj >= b_obj - elif operator == '<': - return a_obj < b_obj - elif operator == '<=': - return a_obj <= b_obj - elif operator == '==': - return a_obj == b_obj + required_has_epoch, current_no_epoch = bool(RE_VERSION_WITH_EPOCH.match(final_required)), RE_VERSION_WITH_EPOCH.split(final_current) + current_has_epoch = len(current_no_epoch) > 1 - raise Exception("compare: invalid string operator {}".format(operator)) + if required_has_epoch and not current_has_epoch: + final_current = '0:{}'.format(final_current) + elif current_has_epoch and not required_has_epoch: + final_current = current_no_epoch[1] + + required_has_release, current_no_release = bool(RE_VERSION_WITH_RELEASE.match(final_required)), RE_VERSION_WITH_RELEASE.split(final_current) + current_has_release = len(current_no_release) > 1 + + if required_has_release and not current_has_release: + final_current = '{}-1'.format(final_current) + elif current_has_release and not required_has_release: + final_current = current_no_release[1] + + final_required, final_current = LegacyVersion(final_required), LegacyVersion(final_current) + + if operator == '==' or operator == '=': + return final_current == final_required + elif operator == '>': + return final_current > final_required + elif operator == '>=': + return final_current >= final_required + elif operator == '<': + return final_current < final_required + elif operator == '<=': + return final_current <= final_required diff --git a/tests/gems/arch/test_aur_data_mapper.py b/tests/gems/arch/test_aur_data_mapper.py index 47c01bef..24946105 100644 --- a/tests/gems/arch/test_aur_data_mapper.py +++ b/tests/gems/arch/test_aur_data_mapper.py @@ -10,7 +10,7 @@ class ArchDataMapperTest(TestCase): @classmethod def setUpClass(cls): - warnings.filterwarnings('ignore') + warnings.filterwarnings('ignore', category=DeprecationWarning) def test_check_version_update(self): self.assertTrue(AURDataMapper.check_version_update('1.0.0-1', '1.0.0-2')) @@ -181,3 +181,17 @@ class ArchDataMapperTest(TestCase): # in this case, install_date will be considered instead of package's last_modified. # as 'install_date' is higher, only the string versions will be compared self.assertTrue(mapper.check_update(pkg=pkg, last_modified=None)) + + def test_check_version_update__one_version_contain_mixed_letters_and_symbols(self): + self.assertTrue(AURDataMapper.check_version_update('2.2.6', '2.3.3op2')) + self.assertFalse(AURDataMapper.check_version_update('2.3', '2.2.a')) + self.assertTrue(AURDataMapper.check_version_update('2.2.a.123', '2.3')) + + def test_check_update__only_installed_version_with_release_number(self): + self.assertTrue(AURDataMapper.check_version_update('2.2.6-1', '2.3')) + self.assertTrue(AURDataMapper.check_version_update('2.2', '2.2-2')) + self.assertFalse(AURDataMapper.check_version_update('2.2', '2.2-1')) + + def test_check_update__versions_with_epoch(self): + self.assertTrue(AURDataMapper.check_version_update('2.3', '1:2.1')) + self.assertFalse(AURDataMapper.check_version_update('1:1.0', '2.1')) diff --git a/tests/gems/arch/test_version.py b/tests/gems/arch/test_version.py index 435ae344..3815e80c 100644 --- a/tests/gems/arch/test_version.py +++ b/tests/gems/arch/test_version.py @@ -4,48 +4,55 @@ from unittest import TestCase from bauh.gems.arch import version -class CompareVersionsTest(TestCase): +class MatchRequiredVersionTest(TestCase): @classmethod def setUpClass(cls): - warnings.filterwarnings('ignore') + warnings.filterwarnings('ignore', category=DeprecationWarning) - def test_both_versions_only_filled_with_numbers_and_same_number_of_divisions(self): - self.assertFalse(version.compare_versions('1', '>', '2')) - self.assertTrue(version.compare_versions('1', '<', '2')) - self.assertTrue(version.compare_versions('1', '<=', '2')) - self.assertTrue(version.compare_versions('2', '>=', '1')) - self.assertTrue(version.compare_versions('1', '==', '1')) - self.assertFalse(version.compare_versions('1.1', '>', '2.0')) - self.assertTrue(version.compare_versions('1.1', '<', '2.0')) - self.assertTrue(version.compare_versions('1.1', '<=', '2.0')) - self.assertTrue(version.compare_versions('2.1', '>', '2.0')) - self.assertTrue(version.compare_versions('2.1', '>=', '2.0')) - self.assertTrue(version.compare_versions('2.0', '==', '2.0')) + def test_must_accept_single_or_both_equal_symbols_as_valid(self): + self.assertTrue(version.match_required_version('1', '=', '1')) + self.assertTrue(version.match_required_version('1', '==', '1')) - def test_versions_only_filled_with_numbers_and_different_number_of_divisons(self): - self.assertFalse(version.compare_versions('1.0', '>', '2')) - self.assertTrue(version.compare_versions('1', '<', '2.0')) - self.assertTrue(version.compare_versions('1.1.1', '<=', '2')) - self.assertTrue(version.compare_versions('2', '>=', '1.0')) - self.assertTrue(version.compare_versions('1.0', '==', '1')) - self.assertTrue(version.compare_versions('1.1', '>', '1')) - self.assertFalse(version.compare_versions('1.1', '<', '1')) + def test_must_ignore_release_number_when_required_has_no_defined(self): + self.assertTrue(version.match_required_version('1.3.0-2', '==', '1.3.0')) + self.assertTrue(version.match_required_version('1.3.0-2', '<=', '1.3.0')) + self.assertTrue(version.match_required_version('1.3.0-2', '>=', '1.3.0')) + self.assertFalse(version.match_required_version('1.3.0-2', '<', '1.3.0')) + self.assertFalse(version.match_required_version('1.3.0-2', '>', '1.3.0')) - def test_one_version_contain_mixed_letters_and_symbols(self): - self.assertTrue(version.compare_versions('2.3.3op2', '>=', '2.2.6')) - self.assertTrue(version.compare_versions('2.2a', '<', '2.2b')) - self.assertTrue(version.compare_versions('2.2a', '<', '2.3a')) + def test_must_ignore_epoch_number_when_required_has_no_defined(self): + self.assertTrue(version.match_required_version('1:1.3.0-1', '>', '1.1.0')) + self.assertTrue(version.match_required_version('1:1.3.0-1', '>=', '1.1.0')) + self.assertFalse(version.match_required_version('1:1.3.0-1', '<', '1.1.0')) + self.assertFalse(version.match_required_version('1:1.3.0-1', '<=', '1.1.0')) + self.assertFalse(version.match_required_version('1:1.3.0-1', '==', '1.1.0')) - def test_versions_with_epochs(self): - self.assertTrue(version.compare_versions('1:1', '==', '1:1.0')) - self.assertTrue(version.compare_versions('1:1', '<', '1:1.1')) - self.assertTrue(version.compare_versions('1:1', '<=', '1:1.1')) - self.assertTrue(version.compare_versions('2:0', '>', '1:1.1')) - self.assertTrue(version.compare_versions('1:0', '>', '2.0-1')) + def test_must_consider_default_epoch_for_current_version_when_required_has_epoch(self): + self.assertFalse(version.match_required_version('1.1.0', '==', '1:1.1.0')) # 1.1.0 -> 0:1.1.0 + self.assertFalse(version.match_required_version('1.1.0', '>', '1:1.1.0')) # 1.1.0 -> 0:1.1.0 + self.assertFalse(version.match_required_version('1.1.0', '>=', '1:1.1.0')) # 1.1.0 -> 0:1.1.0 + self.assertTrue(version.match_required_version('1.1.0', '<', '1:1.1.0')) # 1.1.0 -> 0:1.1.0 + self.assertTrue(version.match_required_version('1.1.0', '<=', '1:1.1.0')) # 1.1.0 -> 0:1.1.0 - def test_versions_with_release_number(self): - self.assertTrue(version.compare_versions('1.0-1', '<', '1.0-2')) - self.assertTrue(version.compare_versions('1.0-2', '>', '1.0-1')) - self.assertTrue(version.compare_versions('1.0-1', '<', '1.0-10')) - self.assertTrue(version.compare_versions('1.0-10', '>', '1.0-1')) + self.assertTrue(version.match_required_version('1.1.0', '==', '0:1.1.0')) # 1.1.0 -> 0:1.1.0 + + def test_must_match_when_current_version_is_composed_of_alphanumerics_but_required_no(self): + self.assertTrue(version.match_required_version('2.3.3op2', '>', '2.2.6')) + self.assertTrue(version.match_required_version('2.3.3op2', '>=', '2.2.6')) + self.assertFalse(version.match_required_version('2.3.3op2', '==', '2.2.6')) + self.assertFalse(version.match_required_version('2.3.3op2', '<', '2.2.6')) + self.assertFalse(version.match_required_version('2.3.3op2', '<=', '2.2.6')) + + # opposite comparisons + self.assertFalse(version.match_required_version('2.2.6', '>', '2.3.3op2')) + self.assertFalse(version.match_required_version('2.2.6', '>=', '2.3.3op2')) + self.assertFalse(version.match_required_version('2.2.6', '==', '2.3.3op2')) + self.assertTrue(version.match_required_version('2.2.6', '<', '2.3.3op2')) + self.assertTrue(version.match_required_version('2.2.6', '<=', '2.3.3op2')) + + def test_must_match_when_both_versions_are_composed_of_alphanumerics(self): + self.assertTrue(version.match_required_version('2.3.3ab', '==', '2.3.3ab')) + self.assertTrue(version.match_required_version('2.3.3ab', '<', '2.3.3ac')) + self.assertTrue(version.match_required_version('2.3.3ad', '>', '2.3.3ac')) + self.assertTrue(version.match_required_version('2.3.3ad', '<', '2.3.3ad.1'))