]> git-server-git.apps.pok.os.sepia.ceph.com Git - teuthology.git/commitdiff
teuthology/task/install: implement LooseVersion and use it
authorKefu Chai <tchaikov@gmail.com>
Thu, 18 Dec 2025 09:05:58 +0000 (17:05 +0800)
committerKefu Chai <tchaikov@gmail.com>
Thu, 18 Dec 2025 13:47:46 +0000 (21:47 +0800)
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 <module>
    from teuthology.orchestra.remote import RemoteShell
  File "/tmp/tmp.xwxq8FOScf/teuthology/teuthology/orchestra/remote.py", line 6, in <module>
    import teuthology.lock.util
  File "/tmp/tmp.xwxq8FOScf/teuthology/teuthology/lock/util.py", line 6, in <module>
    import teuthology.provision.downburst
  File "/tmp/tmp.xwxq8FOScf/teuthology/teuthology/provision/__init__.py", line 4, in <module>
    import teuthology.exporter
  File "/tmp/tmp.xwxq8FOScf/teuthology/teuthology/exporter.py", line 11, in <module>
    import teuthology.dispatcher
  File "/tmp/tmp.xwxq8FOScf/teuthology/teuthology/dispatcher/__init__.py", line 22, in <module>
    from teuthology.dispatcher import supervisor
  File "/tmp/tmp.xwxq8FOScf/teuthology/teuthology/dispatcher/supervisor.py", line 18, in <module>
    from teuthology.task import internal
  File "/tmp/tmp.xwxq8FOScf/teuthology/teuthology/task/internal/__init__.py", line 27, in <module>
    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 <module>
    from teuthology.task.install.redhat import set_deb_repo
  File "/tmp/tmp.xwxq8FOScf/teuthology/teuthology/task/install/__init__.py", line 14, in <module>
    from distutils.version import LooseVersion
ModuleNotFoundError: No module named 'distutils'
```

Related: https://peps.python.org/pep-0632/

Signed-off-by: Kefu Chai <tchaikov@gmail.com>
teuthology/task/install/__init__.py
teuthology/task/install/rpm.py
teuthology/util/version.py [new file with mode: 0644]

index c726e3d74076593218c66ea627b787e40483218d..ebe18d043c516e6dbda662d835726cb0ddbceb14 100644 (file)
@@ -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__)
 
index 617f8daaf391038f70ee094569be213db4e810c8..206764e70659390ce9ab4ac1497586998ca3722e 100644 (file)
@@ -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 (file)
index 0000000..a7657f3
--- /dev/null
@@ -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))