From: Kefu Chai Date: Thu, 18 Dec 2025 09:05:58 +0000 (+0800) Subject: teuthology/task/install: implement LooseVersion and use it X-Git-Tag: 1.2.3~11^2~1 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=90ad9bab15deb70ecdba9514aedc521e566d1aee;p=teuthology.git teuthology/task/install: implement LooseVersion and use it The distutils module was deprecated in Python 3.10 and removed in Python 3.12. This commit replaces the deprecated distutils.version imports with the a homebrew LooseVersion implementation. Changes: - implement LooseVersion which is able to parse versions like '10.2.2-63-g8542898-1trusty'. - Replace distutils.version.LooseVersion with teuthology.util.version.LooseVersion packaging.version.LooseVersion Fixes: ``` Traceback (most recent call last): File "/home/jenkins-build/build/workspace/ceph-api/build/../qa/tasks/vstart_runner.py", line 81, in from teuthology.orchestra.remote import RemoteShell File "/tmp/tmp.xwxq8FOScf/teuthology/teuthology/orchestra/remote.py", line 6, in import teuthology.lock.util File "/tmp/tmp.xwxq8FOScf/teuthology/teuthology/lock/util.py", line 6, in import teuthology.provision.downburst File "/tmp/tmp.xwxq8FOScf/teuthology/teuthology/provision/__init__.py", line 4, in import teuthology.exporter File "/tmp/tmp.xwxq8FOScf/teuthology/teuthology/exporter.py", line 11, in import teuthology.dispatcher File "/tmp/tmp.xwxq8FOScf/teuthology/teuthology/dispatcher/__init__.py", line 22, in from teuthology.dispatcher import supervisor File "/tmp/tmp.xwxq8FOScf/teuthology/teuthology/dispatcher/supervisor.py", line 18, in from teuthology.task import internal File "/tmp/tmp.xwxq8FOScf/teuthology/teuthology/task/internal/__init__.py", line 27, in from teuthology.task.internal.redhat import (setup_cdn_repo, setup_base_repo, # noqa File "/tmp/tmp.xwxq8FOScf/teuthology/teuthology/task/internal/redhat.py", line 13, in from teuthology.task.install.redhat import set_deb_repo File "/tmp/tmp.xwxq8FOScf/teuthology/teuthology/task/install/__init__.py", line 14, in from distutils.version import LooseVersion ModuleNotFoundError: No module named 'distutils' ``` Related: https://peps.python.org/pep-0632/ Signed-off-by: Kefu Chai --- diff --git a/teuthology/task/install/__init__.py b/teuthology/task/install/__init__.py index c726e3d74..ebe18d043 100644 --- a/teuthology/task/install/__init__.py +++ b/teuthology/task/install/__init__.py @@ -10,12 +10,12 @@ from teuthology import contextutil, packaging from teuthology.parallel import parallel from teuthology.task import ansible -from distutils.version import LooseVersion from teuthology.task.install.util import ( _get_builder_project, get_flavor, ship_utilities, ) from teuthology.task.install import rpm, deb, redhat +from teuthology.util.version import LooseVersion log = logging.getLogger(__name__) diff --git a/teuthology/task/install/rpm.py b/teuthology/task/install/rpm.py index 617f8daaf..206764e70 100644 --- a/teuthology/task/install/rpm.py +++ b/teuthology/task/install/rpm.py @@ -2,14 +2,13 @@ import logging import os.path from io import StringIO -from distutils.version import LooseVersion - from teuthology.config import config as teuth_config from teuthology.contextutil import safe_while from teuthology.orchestra import run from teuthology import packaging from teuthology.task.install.util import _get_builder_project, _get_local_dir +from teuthology.util.version import LooseVersion log = logging.getLogger(__name__) diff --git a/teuthology/util/version.py b/teuthology/util/version.py new file mode 100644 index 000000000..a7657f311 --- /dev/null +++ b/teuthology/util/version.py @@ -0,0 +1,49 @@ +import re +from functools import total_ordering +from typing import Union, List + + +@total_ordering +class LooseVersion: + """ + A flexible version comparison class that handles arbitrary version strings. + Compares numeric components numerically and alphabetic components lexically. + """ + + _component_re = re.compile(r'(\d+|[a-z]+|\.)', re.IGNORECASE) + + def __init__(self, vstring: str): + self.vstring = str(vstring) + self.version = self._parse(self.vstring) + + def _parse(self, vstring: str) -> List[Union[int, str]]: + """Parse version string into comparable components.""" + components = [] + for match in self._component_re.finditer(vstring.lower()): + component = match.group() + if component != '.': + # Try to convert to int, fall back to string + try: + components.append(int(component)) + except ValueError: + components.append(component) + return components + + def __str__(self) -> str: + return self.vstring + + def __repr__(self) -> str: + return f"{self.__class__.__name__}('{self.vstring}')" + + def __eq__(self, other) -> bool: + if not isinstance(other, LooseVersion): + other = LooseVersion(str(other)) + return self.version == other.version + + def __lt__(self, other) -> bool: + if not isinstance(other, LooseVersion): + other = LooseVersion(str(other)) + return self.version < other.version + + def __hash__(self) -> int: + return hash(tuple(self.version))