This is an automated email from the ASF dual-hosted git repository.

mck pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-ccm.git

commit 27e46a1625453a77b1fca108fea8704644f48c73
Author: Dmitry Kropachev <[email protected]>
AuthorDate: Sun Dec 7 03:52:20 2025 -0400

    Replace distutils.version with custom implementation
    
    distutils.version is getting depricated, we need to move off it.
    
     patch by Dmitry Kropachev; reviewed by Mick Semb Wever for CASSANDRA-18321
---
 AGENTS.md                 | 30 +++++++++++++++++++
 ccmlib/cluster.py         |  2 +-
 ccmlib/cluster_factory.py |  3 +-
 ccmlib/common.py          |  2 +-
 ccmlib/node.py            |  2 +-
 ccmlib/repository.py      |  2 +-
 ccmlib/version.py         | 74 +++++++++++++++++++++++++++++++++++++++++++++++
 requirements.txt          |  2 +-
 tests/test_common.py      |  2 +-
 tests/test_lib.py         |  2 +-
 10 files changed, 112 insertions(+), 9 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..6815fb5
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,30 @@
+# Repository Guidelines
+
+## Project Structure & Module Organization
+- CLI entry points live in `ccm/` and the core implementation is under 
`ccmlib/` (cluster lifecycle, node management, command helpers).
+- Tests are in `tests/` and mirror library modules; add new tests alongside 
the code they cover.
+- Packaging and install metadata: `setup.py`, `setup.cfg`, and runtime deps in 
`requirements.txt`. Miscellaneous utilities and docs live in `misc/`, `ssl/`, 
and `INSTALL.md`.
+
+## Build, Test, and Development Commands
+- Create an isolated environment: `python3 -m venv venv && source 
venv/bin/activate`.
+- Install runtime deps and editable package: `pip install -r requirements.txt 
&& pip install -e .`.
+- Add test-only deps: `pip install mock pytest requests`.
+- Run tests: `pytest` (use `pytest tests/test_node.py -k <pattern>` for 
focused runs). Expect integration-like cases that spawn local Cassandra nodes; 
keep environments clean.
+- Validate a local install: `ccm --help` to confirm entry points resolve.
+
+## Coding Style & Naming Conventions
+- Python-first codebase; follow PEP 8 with 4-space indents and line-length 
restraint (~100 chars).
+- Maintain Python 2.7 and 3.x compatibility where practical; avoid newer 
syntax that breaks 2.7 and gate version-specific behavior carefully.
+- Use descriptive identifiers for nodes/clusters, and align CLI option names 
with existing verbs/nouns (`create`, `populate`, `start`, etc.).
+- Prefer pure functions for helpers; keep side effects (filesystem, 
subprocess) localized and well-logged.
+
+## Testing Guidelines
+- Framework: pytest. Name files `test_*.py` and favor small, deterministic 
cases over long-lived clusters.
+- When tests need Cassandra binaries, prefer the repository cache 
(`~/.ccm/repository`) to avoid repeated downloads.
+- Add coverage when touching node lifecycle, logging, or install path 
resolution; include regression tests reproducing reported issues.
+- Use markers or narrow selections for slow tests; do not assume network 
availability beyond local loopback.
+
+## Commit & Pull Request Guidelines
+- Commits are short, action-oriented summaries (e.g., `Improve jdk 
validation`, `Fix cleanup when node start fails`); keep a single focus per 
commit.
+- PRs should describe the change, note risk areas (filesystem writes, 
subprocess calls), and link related tickets/issues.
+- Include how you tested (`pytest`, manual `ccm` invocation) and any 
environment notes (Python version, OS). Screenshots are unnecessary; logs or 
command transcripts help reviewers.
diff --git a/ccmlib/cluster.py b/ccmlib/cluster.py
index 6c2d27a..213eb85 100644
--- a/ccmlib/cluster.py
+++ b/ccmlib/cluster.py
@@ -28,7 +28,7 @@ import subprocess
 import threading
 import time
 from collections import OrderedDict, defaultdict, namedtuple
-from distutils.version import LooseVersion #pylint: disable=import-error, 
no-name-in-module
+from ccmlib.version import LooseVersion
 
 import yaml
 from six import print_
diff --git a/ccmlib/cluster_factory.py b/ccmlib/cluster_factory.py
index fb90e59..e3b5222 100644
--- a/ccmlib/cluster_factory.py
+++ b/ccmlib/cluster_factory.py
@@ -24,8 +24,7 @@ import yaml
 
 from ccmlib import common, extension, repository
 from ccmlib.node import Node
-
-from distutils.version import LooseVersion  #pylint: disable=import-error, 
no-name-in-module
+from ccmlib.version import LooseVersion
 
 class ClusterFactory():
 
diff --git a/ccmlib/common.py b/ccmlib/common.py
index c297a85..5d4b4aa 100644
--- a/ccmlib/common.py
+++ b/ccmlib/common.py
@@ -35,7 +35,7 @@ import subprocess
 import sys
 import time
 import yaml
-from distutils.version import LooseVersion  #pylint: disable=import-error, 
no-name-in-module
+from ccmlib.version import LooseVersion
 from six import print_
 
 from ccmlib import extension
diff --git a/ccmlib/node.py b/ccmlib/node.py
index 4c46a38..cc09eab 100644
--- a/ccmlib/node.py
+++ b/ccmlib/node.py
@@ -35,7 +35,7 @@ import time
 import warnings
 from collections import namedtuple
 from datetime import datetime
-from distutils.version import LooseVersion  #pylint: disable=import-error, 
no-name-in-module
+from ccmlib.version import LooseVersion
 
 import yaml
 from six import print_, string_types
diff --git a/ccmlib/repository.py b/ccmlib/repository.py
index 9b41fe9..62e9c0f 100644
--- a/ccmlib/repository.py
+++ b/ccmlib/repository.py
@@ -30,7 +30,7 @@ import sys
 import tarfile
 import tempfile
 import time
-from distutils.version import LooseVersion  # pylint: disable=import-error, 
no-name-in-module
+from ccmlib.version import LooseVersion
 
 from six import next, print_
 
diff --git a/ccmlib/version.py b/ccmlib/version.py
new file mode 100644
index 0000000..7187159
--- /dev/null
+++ b/ccmlib/version.py
@@ -0,0 +1,74 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+
+import re
+from functools import total_ordering
+
+from packaging.version import parse
+
+
+@total_ordering
+class LooseVersion(object):
+    """
+    Lightweight compatibility wrapper to replace 
distutils.version.LooseVersion.
+
+    It delegates comparison/ordering to packaging.version.parse while exposing
+    ``version`` and ``vstring`` attributes that the existing code expects.
+    """
+
+    __slots__ = ("_inner",)
+
+    def __init__(self, version):
+        if isinstance(version, LooseVersion):
+            self._inner = version._inner  # pylint: disable=protected-access
+        else:
+            self._inner = parse(str(version))
+
+    def __repr__(self):
+        return "LooseVersion({})".format(str(self._inner))
+
+    def __str__(self):
+        return str(self._inner)
+
+    def __hash__(self):
+        return hash(self._inner)
+
+    def _coerce_other(self, other):
+        if isinstance(other, LooseVersion):
+            return other._inner  # pylint: disable=protected-access
+        return parse(str(other))
+
+    def __eq__(self, other):
+        return self._inner == self._coerce_other(other)
+
+    def __lt__(self, other):
+        return self._inner < self._coerce_other(other)
+
+    @property
+    def vstring(self):
+        return str(self._inner)
+
+    @property
+    def version(self):
+        release = getattr(self._inner, "release", None)
+        if release:
+            return release
+
+        # Fallback for legacy version objects without a release tuple
+        parts = re.split(r"\D+", str(self._inner))
+        return tuple(int(p) for p in parts if p)
diff --git a/requirements.txt b/requirements.txt
index c8153c0..45e818b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -18,4 +18,4 @@ pyYaml<5.4; python_version < '3'
 pyYaml; python_version >= '3'
 six >=1.4.1
 psutil
-
+packaging<21
diff --git a/tests/test_common.py b/tests/test_common.py
index 0ecf52f..9760d25 100644
--- a/tests/test_common.py
+++ b/tests/test_common.py
@@ -18,7 +18,7 @@
 import unittest
 from mock import patch
 
-from distutils.version import LooseVersion
+from ccmlib.version import LooseVersion
 
 from ccmlib import common
 from . import ccmtest
diff --git a/tests/test_lib.py b/tests/test_lib.py
index 88ce9bd..a79c64a 100644
--- a/tests/test_lib.py
+++ b/tests/test_lib.py
@@ -29,8 +29,8 @@ import ccmlib
 from ccmlib.cluster import Cluster
 from ccmlib.common import _update_java_version, 
get_supported_jdk_versions_from_dist, get_supported_jdk_versions, 
get_available_jdk_versions
 from ccmlib.node import NodeError
+from ccmlib.version import LooseVersion
 from . import TEST_DIR, ccmtest
-from distutils.version import LooseVersion  # pylint: disable=import-error, 
no-name-in-module
 
 sys.path = [".."] + sys.path
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to