From d84e30d44ee909f76c0f338b399eb3d0def8a3f4 Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Mon, 14 Oct 2024 23:07:09 +0900 Subject: [PATCH] Vendor VersionPredicate Add simplified VersionPredicate to replace distutils.versionpredicate. Note that the predicate format is simplified and does not contain the package name, because this field is not actually used. Related-Bug: #2083518 Change-Id: I5eb3601650e536a1f4c290bb68d75d5a232f9141 --- oslo_utils/tests/test_versionutils.py | 48 +++++++++++++++++++ oslo_utils/versionutils.py | 33 +++++++++++++ .../version-predicate-42f38f7b7e9187e1.yaml | 6 +++ 3 files changed, 87 insertions(+) create mode 100644 releasenotes/notes/version-predicate-42f38f7b7e9187e1.yaml diff --git a/oslo_utils/tests/test_versionutils.py b/oslo_utils/tests/test_versionutils.py index 35c5bcd1..23d96e14 100644 --- a/oslo_utils/tests/test_versionutils.py +++ b/oslo_utils/tests/test_versionutils.py @@ -93,3 +93,51 @@ class IsCompatibleTestCase(test_base.BaseTestCase): versionutils.convert_version_to_tuple('6.7.0beta1')) self.assertEqual((6, 7, 0), versionutils.convert_version_to_tuple('6.7.0rc1')) + + +class VersionPredicateTest(test_base.BaseTestCase): + def test_version_predicate_valid(self): + pred = versionutils.VersionPredicate('<2.0.0') + self.assertTrue(pred.satisfied_by('1.10.0')) + self.assertFalse(pred.satisfied_by('2.0.0')) + + pred = versionutils.VersionPredicate('<=2.0.0') + self.assertTrue(pred.satisfied_by('2.0.0')) + self.assertFalse(pred.satisfied_by('2.1.0')) + + pred = versionutils.VersionPredicate('>2.0.0') + self.assertFalse(pred.satisfied_by('1.10.0')) + self.assertTrue(pred.satisfied_by('2.1.0')) + + pred = versionutils.VersionPredicate('>=2.0.0') + self.assertFalse(pred.satisfied_by('1.9.0')) + self.assertTrue(pred.satisfied_by('2.0.0')) + + pred = versionutils.VersionPredicate('== 2.0.0') + self.assertFalse(pred.satisfied_by('1.9.9')) + self.assertTrue(pred.satisfied_by('2.0.0')) + self.assertFalse(pred.satisfied_by('2.0.1')) + + pred = versionutils.VersionPredicate('!= 2.0.0') + self.assertTrue(pred.satisfied_by('1.9.9')) + self.assertFalse(pred.satisfied_by('2.0.0')) + self.assertTrue(pred.satisfied_by('2.0.1')) + + def test_version_predicate_valid_multi(self): + pred = versionutils.VersionPredicate('<3.0.0,>=2.1.0') + self.assertTrue(pred.satisfied_by('2.1.0')) + self.assertTrue(pred.satisfied_by('2.11.0')) + self.assertFalse(pred.satisfied_by('2.0.0')) + self.assertFalse(pred.satisfied_by('3.0.0')) + + pred = versionutils.VersionPredicate(' < 2.0.0, >= 1.0.0 ') + self.assertTrue(pred.satisfied_by('1.0.0')) + self.assertTrue(pred.satisfied_by('1.10.0')) + self.assertFalse(pred.satisfied_by('0.9.0')) + self.assertFalse(pred.satisfied_by('2.0.0')) + + def test_version_predicate_valid_invalid(self): + for invalid_str in ['3.0.0', 'foo', '<> 3.0.0', '>=1.0.0;<2.0.0', + '>abc', '>=1.0.0,', '>=1.0.0,2.0.0']: + self.assertRaises( + ValueError, versionutils.VersionPredicate, invalid_str) diff --git a/oslo_utils/versionutils.py b/oslo_utils/versionutils.py index 3c6a86ca..4ef7afbf 100644 --- a/oslo_utils/versionutils.py +++ b/oslo_utils/versionutils.py @@ -20,6 +20,7 @@ Helpers for comparing version strings. """ import functools +import operator import re import packaging.version @@ -90,3 +91,35 @@ def convert_version_to_tuple(version_str): """ version_str = re.sub(r'(\d+)(a|alpha|b|beta|rc)\d+$', '\\1', version_str) return tuple(int(part) for part in version_str.split('.')) + + +class VersionPredicate: + """Parse version predicate and check version requirements + + This is based on the implementation of distutils.VersionPredicate + + .. versionadded:: 7.4 + """ + _PREDICATE_MATCH = re.compile(r"^\s*(<=|>=|<|>|!=|==)\s*([^\s]+)\s*$") + _COMP_MAP = { + "<": operator.lt, "<=": operator.le, "==": operator.eq, + ">": operator.gt, ">=": operator.ge, "!=": operator.ne + } + + def __init__(self, predicate_str): + self.pred = [self._parse_predicate(pred) for pred + in predicate_str.split(',')] + + def _parse_predicate(self, pred): + res = self._PREDICATE_MATCH.match(pred) + if not res: + raise ValueError("bad package restriction syntax: %s" % pred) + cond, ver_str = res.groups() + return (cond, packaging.version.Version(ver_str)) + + def satisfied_by(self, version_str): + version = packaging.version.Version(version_str) + for cond, ver in self.pred: + if not self._COMP_MAP[cond](version, ver): + return False + return True diff --git a/releasenotes/notes/version-predicate-42f38f7b7e9187e1.yaml b/releasenotes/notes/version-predicate-42f38f7b7e9187e1.yaml new file mode 100644 index 00000000..38f37e83 --- /dev/null +++ b/releasenotes/notes/version-predicate-42f38f7b7e9187e1.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + The new ``VersionPredicate`` class has been added to the ``versionutils`` + module, which parses version predicate and check if the given version meets + the described requirements.