This is an automated email from the ASF dual-hosted git repository.
absurdfarce pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-python-driver.git
The following commit(s) were added to refs/heads/trunk by this push:
new 95e2517d CASSPYTHON-10 Update cassandra.util.Version to better support
Cassandra version strings
95e2517d is described below
commit 95e2517dc6e860ff84535443d568cd03c602e409
Author: absurdfarce <[email protected]>
AuthorDate: Wed Feb 25 16:19:04 2026 -0600
CASSPYTHON-10 Update cassandra.util.Version to better support Cassandra
version strings
patch by Bret McGuire; reviewed by Brad Schoening and Bret McGuire
---
cassandra/metadata.py | 2 +-
cassandra/util.py | 118 +++++++++++++++++-------------------------
tests/unit/test_util_types.py | 95 ++++++++++++++++++++--------------
3 files changed, 104 insertions(+), 111 deletions(-)
diff --git a/cassandra/metadata.py b/cassandra/metadata.py
index 2c13f92e..0b403bc3 100644
--- a/cassandra/metadata.py
+++ b/cassandra/metadata.py
@@ -3290,7 +3290,7 @@ def get_schema_parser(connection, server_version,
dse_version, timeout):
elif v >= Version('6.0.0'):
return SchemaParserDSE60(connection, timeout)
- if version >= Version('4-a'):
+ if version >= Version('4.0-alpha'):
return SchemaParserV4(connection, timeout)
elif version >= Version('3.0.0'):
return SchemaParserV3(connection, timeout)
diff --git a/cassandra/util.py b/cassandra/util.py
index f9739125..408211ed 100644
--- a/cassandra/util.py
+++ b/cassandra/util.py
@@ -1692,57 +1692,56 @@ class DateRange(object):
self.lower_bound, self.upper_bound, self.value
)
+VERSION_REGEX =
re.compile("^(\\d+)\\.(\\d+)(\\.\\d+)?(\\.\\d+)?([~\\-]\\w[.\\w]*(?:-\\w[.\\w]*)*)?(\\+[.\\w]+)?$")
@total_ordering
class Version(object):
"""
- Internal minimalist class to compare versions.
- A valid version is: <int>.<int>.<int>.<int or str>.
+ Representation of a Cassandra version. Mostly follows the implementation
of the same logic in the Java driver;
+ see
https://github.com/apache/cassandra-java-driver/blob/4.19.2/core/src/main/java/com/datastax/oss/driver/api/core/Version.java.
- TODO: when python2 support is removed, use packaging.version.
+ Cassandra versions are assumed to correspond to major.minor.patch with an
optional additional numeric build field as well as a
+ string prerelease field.
"""
- _version = None
- major = None
- minor = 0
- patch = 0
- build = 0
- prerelease = 0
-
def __init__(self, version):
self._version = version
- if '-' in version:
- version_without_prerelease, self.prerelease = version.split('-', 1)
- else:
- version_without_prerelease = version
- parts = list(reversed(version_without_prerelease.split('.')))
- if len(parts) > 4:
- prerelease_string = "-{}".format(self.prerelease) if
self.prerelease else ""
- log.warning("Unrecognized version: {}. Only 4 components plus
prerelease are supported. "
- "Assuming version as {}{}".format(version,
'.'.join(parts[:-5:-1]), prerelease_string))
+
+ match = VERSION_REGEX.match(version)
+ if not match:
+ raise ValueError("Version string {0} did not match expected
format".format(version))
+
+ self.major = int(match[1])
+ self.minor = int(match[2])
try:
- self.major = int(parts.pop())
- except ValueError as e:
- raise ValueError(
- "Couldn't parse version {}. Version should start with a
number".format(version))\
- .with_traceback(e.__traceback__)
+ self.patch = self._cleanup_int(match[3])
+ except:
+ self.patch = 0
+
try:
- self.minor = int(parts.pop()) if parts else 0
- self.patch = int(parts.pop()) if parts else 0
+ self.build = self._cleanup_int(match[4])
+ except:
+ self.build = 0
- if parts: # we have a build version
- build = parts.pop()
- try:
- self.build = int(build)
- except ValueError:
- self.build = build
- except ValueError:
- assumed_version = "{}.{}.{}.{}-{}".format(self.major, self.minor,
self.patch, self.build, self.prerelease)
- log.warning("Unrecognized version {}. Assuming version as
{}".format(version, assumed_version))
+ try:
+ self.prerelease = self._cleanup_str(match[5])
+ except:
+ self.prerelease = ""
+
+ # This is used in a few places below so let's just build it now
+ self._tuple = (self.major, self.minor, self.patch, self.build,
self.prerelease)
+
+ # Trim off the leading '.' characters and convert the discovered value to
an integer
+ def _cleanup_int(self, instr):
+ return int(instr[1:]) if instr else 0
+
+ # Trim off the leading '.' or '~' characters and just return the string
directly
+ def _cleanup_str(self, instr):
+ return instr[1:] if instr else ""
def __hash__(self):
- return self._version
+ return hash(self._tuple)
def __repr__(self):
version_string = "Version({0}, {1}, {2}".format(self.major,
self.minor, self.patch)
@@ -1757,48 +1756,27 @@ class Version(object):
def __str__(self):
return self._version
- @staticmethod
- def _compare_version_part(version, other_version, cmp):
- if not (isinstance(version, int) and
- isinstance(other_version, int)):
- version = str(version)
- other_version = str(other_version)
-
- return cmp(version, other_version)
-
+ # Methods below leverage left-to-right positional comparison of tuples
def __eq__(self, other):
if not isinstance(other, Version):
return NotImplemented
- return (self.major == other.major and
- self.minor == other.minor and
- self.patch == other.patch and
- self._compare_version_part(self.build, other.build, lambda s,
o: s == o) and
- self._compare_version_part(self.prerelease, other.prerelease,
lambda s, o: s == o)
- )
+ return self._tuple == other._tuple
def __gt__(self, other):
if not isinstance(other, Version):
return NotImplemented
- is_major_ge = self.major >= other.major
- is_minor_ge = self.minor >= other.minor
- is_patch_ge = self.patch >= other.patch
- is_build_gt = self._compare_version_part(self.build, other.build,
lambda s, o: s > o)
- is_build_ge = self._compare_version_part(self.build, other.build,
lambda s, o: s >= o)
-
- # By definition, a prerelease comes BEFORE the actual release, so if a
version
- # doesn't have a prerelease, it's automatically greater than anything
that does
- if self.prerelease and not other.prerelease:
- is_prerelease_gt = False
+ # We start by comparing the first four fields directly
+ self_tuple = self._tuple[:4]
+ other_tuple = (other.major, other.minor, other.patch, other.build)
+ if self_tuple != other_tuple:
+ return self_tuple > other_tuple
+ # If we're still around we have to check prereleases... prereleases
always come before
+ # the corresponding version
+ elif self.prerelease and not other.prerelease:
+ return False
elif other.prerelease and not self.prerelease:
- is_prerelease_gt = True
+ return True
else:
- is_prerelease_gt = self._compare_version_part(self.prerelease,
other.prerelease, lambda s, o: s > o) \
-
- return (self.major > other.major or
- (is_major_ge and self.minor > other.minor) or
- (is_major_ge and is_minor_ge and self.patch > other.patch) or
- (is_major_ge and is_minor_ge and is_patch_ge and is_build_gt)
or
- (is_major_ge and is_minor_ge and is_patch_ge and is_build_ge
and is_prerelease_gt)
- )
+ return self.prerelease > other.prerelease
diff --git a/tests/unit/test_util_types.py b/tests/unit/test_util_types.py
index 779d4169..ead02729 100644
--- a/tests/unit/test_util_types.py
+++ b/tests/unit/test_util_types.py
@@ -209,18 +209,13 @@ class VersionTests(unittest.TestCase):
def test_version_parsing(self):
versions = [
- ('2.0.0', (2, 0, 0, 0, 0)),
- ('3.1.0', (3, 1, 0, 0, 0)),
- ('2.4.54', (2, 4, 54, 0, 0)),
- ('3.1.1.12', (3, 1, 1, 12, 0)),
- ('3.55.1.build12', (3, 55, 1, 'build12', 0)),
- ('3.55.1.20190429-TEST', (3, 55, 1, 20190429, 'TEST')),
- ('4.0-SNAPSHOT', (4, 0, 0, 0, 'SNAPSHOT')),
- ('1.0.5.4.3', (1, 0, 5, 4, 0)),
- ('1-SNAPSHOT', (1, 0, 0, 0, 'SNAPSHOT')),
- ('4.0.1.2.3.4.5-ABC-123-SNAP-TEST.blah', (4, 0, 1, 2,
'ABC-123-SNAP-TEST.blah')),
- ('2.1.hello', (2, 1, 0, 0, 0)),
- ('2.test.1', (2, 0, 0, 0, 0)),
+ # Test cases here adapted from the Java driver cases
+ #
(https://github.com/apache/cassandra-java-driver/blob/4.19.2/core/src/test/java/com/datastax/oss/driver/api/core/VersionTest.java)
+ ('1.2.19', (1, 2, 19, 0, "")),
+ ('1.2', (1, 2, 0, 0, "")),
+ ('1.2-beta1-SNAPSHOT', (1, 2, 0, 0, 'beta1-SNAPSHOT')),
+ ('1.2~beta1-SNAPSHOT', (1, 2, 0, 0, 'beta1-SNAPSHOT')),
+ ('1.2.19.2-SNAPSHOT', (1, 2, 19, 2, 'SNAPSHOT')),
]
for str_version, expected_result in versions:
@@ -232,9 +227,17 @@ class VersionTests(unittest.TestCase):
self.assertEqual(v.build, expected_result[3])
self.assertEqual(v.prerelease, expected_result[4])
- # not supported version formats
- with self.assertRaises(ValueError):
- Version('test.1.0')
+ # Note that a few of these formats used to be supported when this
class was based on the Python versioning scheme.
+ # This has been updated to more directly correspond to the Cassandra
versioning scheme. See CASSPYTHON-10 for more
+ # detail.
+ unsupported_versions = [
+ "test.1.0",
+ '2.test.1'
+ ]
+
+ for v in unsupported_versions:
+ with self.assertRaises(ValueError):
+ Version(v)
def test_version_compare(self):
# just tests a bunch of versions
@@ -251,41 +254,53 @@ class VersionTests(unittest.TestCase):
# patch wins
self.assertTrue(Version('2.3.1') > Version('2.3.0'))
- self.assertTrue(Version('2.3.1') > Version('2.3.0.4post0'))
+ self.assertTrue(Version('2.3.1') > Version('2.3.0-4post0'))
self.assertTrue(Version('2.3.1') > Version('2.3.0.44'))
# various
self.assertTrue(Version('2.3.0.1') > Version('2.3.0.0'))
self.assertTrue(Version('2.3.0.680') > Version('2.3.0.670'))
self.assertTrue(Version('2.3.0.681') > Version('2.3.0.680'))
- self.assertTrue(Version('2.3.0.1build0') > Version('2.3.0.1')) # 4th
part fallback to str cmp
- self.assertTrue(Version('2.3.0.build0') > Version('2.3.0.1')) # 4th
part fallback to str cmp
- self.assertTrue(Version('2.3.0') < Version('2.3.0.build'))
-
- self.assertTrue(Version('4-a') <= Version('4.0.0'))
- self.assertTrue(Version('4-a') <= Version('4.0-alpha1'))
- self.assertTrue(Version('4-a') <= Version('4.0-beta1'))
- self.assertTrue(Version('4.0.0') >= Version('4.0.0'))
- self.assertTrue(Version('4.0.0.421') >= Version('4.0.0'))
- self.assertTrue(Version('4.0.1') >= Version('4.0.0'))
+
+ # If builds are equal then a prerelease always comes before
+ self.assertTrue(Version('2.3.0.1-SNAPSHOT') < Version('2.3.0.1'))
+
+ # If both have prereleases we fall back to a string compare
+ self.assertTrue(Version('2.3.0.1-SNAPSHOT') <
Version('2.3.0.1-ZNAPSHOT'))
+
self.assertTrue(Version('2.3.0') == Version('2.3.0'))
self.assertTrue(Version('2.3.32') == Version('2.3.32'))
self.assertTrue(Version('2.3.32') == Version('2.3.32.0'))
- self.assertTrue(Version('2.3.0.build') == Version('2.3.0.build'))
+ self.assertTrue(Version('2.3.0-SNAPSHOT') == Version('2.3.0-SNAPSHOT'))
- self.assertTrue(Version('4') == Version('4.0.0'))
self.assertTrue(Version('4.0') == Version('4.0.0.0'))
self.assertTrue(Version('4.0') > Version('3.9.3'))
- self.assertTrue(Version('4.0') > Version('4.0-SNAPSHOT'))
- self.assertTrue(Version('4.0-SNAPSHOT') == Version('4.0-SNAPSHOT'))
- self.assertTrue(Version('4.0.0-SNAPSHOT') == Version('4.0-SNAPSHOT'))
- self.assertTrue(Version('4.0.0-SNAPSHOT') == Version('4.0.0-SNAPSHOT'))
- self.assertTrue(Version('4.0.0.build5-SNAPSHOT') ==
Version('4.0.0.build5-SNAPSHOT'))
- self.assertTrue(Version('4.1-SNAPSHOT') > Version('4.0-SNAPSHOT'))
- self.assertTrue(Version('4.0.1-SNAPSHOT') > Version('4.0.0-SNAPSHOT'))
- self.assertTrue(Version('4.0.0.build6-SNAPSHOT') >
Version('4.0.0.build5-SNAPSHOT'))
- self.assertTrue(Version('4.0-SNAPSHOT2') > Version('4.0-SNAPSHOT1'))
- self.assertTrue(Version('4.0-SNAPSHOT2') > Version('4.0.0-SNAPSHOT1'))
-
- self.assertTrue(Version('4.0.0-alpha1-SNAPSHOT') >
Version('4.0.0-SNAPSHOT'))
+
+ equal_tuples = [
+ (Version('4.0-SNAPSHOT'), Version('4.0-SNAPSHOT')),
+ (Version('4.0.0-SNAPSHOT'), Version('4.0-SNAPSHOT')),
+ (Version('4.0.0-SNAPSHOT'), Version('4.0.0-SNAPSHOT')),
+ (Version('4.0.0.5-SNAPSHOT'), Version('4.0.0.5-SNAPSHOT'))
+ ]
+ for (a,b) in equal_tuples:
+ self.assertEqual(a, b)
+ self.assertEqual(hash(a), hash(b))
+
+ left_greater_tuples = [
+ (Version('4.0'), Version('4.0-SNAPSHOT')),
+ (Version('4.1-SNAPSHOT'), Version('4.0-SNAPSHOT')),
+ (Version('4.0.1-SNAPSHOT'), Version('4.0.0-SNAPSHOT')),
+ (Version('4.0.0.6-SNAPSHOT'), Version('4.0.0.5-SNAPSHOT')),
+ (Version('4.0-SNAPSHOT2'), Version('4.0-SNAPSHOT1')),
+ (Version('4.0-SNAPSHOT2'), Version('4.0.0-SNAPSHOT1')),
+ (Version('4.0.0-alpha1-SNAPSHOT'), Version('4.0.0-SNAPSHOT'))
+ ]
+ for (a,b) in left_greater_tuples:
+ self.assertGreater(a, b)
+
+ # Test the version limit for v4 schema parsing in cassandra.metadata
to make sure
+ # all 4.0.x Cassandra servers are covered
+ self.assertTrue(Version('4.0-alpha') <= Version('4.0.0'))
+ self.assertTrue(Version('4.0-alpha') <= Version('4.0-alpha1'))
+ self.assertTrue(Version('4.0-alpha') <= Version('4.0-beta1'))
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]