Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cassandra/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
109 changes: 41 additions & 68 deletions cassandra/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -1692,54 +1692,43 @@ def __repr__(self):
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>.

TODO: when python2 support is removed, use packaging.version.
"""

_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 did not match expected format")

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 = 0

# Trim off the leading '.' characters and convert the discovered value to an integer
def _cleanup_int(self, s):
return int(s[1:]) if s else 0

# Trim off the leading '.' or '~' characters and just return the string directly
def _cleanup_str(self, str):
return str[1:] if str else 0

def __hash__(self):
return self._version
Expand All @@ -1757,48 +1746,32 @@ def __repr__(self):
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)

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)
self.build == other.build and
self.prerelease == other.prerelease
)

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
if self.major != other.major:
return self.major > other.major
elif self.minor != other.minor:
return self.minor > other.minor
elif self.patch != other.patch:
return self.patch > other.patch
elif self.build != other.build:
return self.build > other.build
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
73 changes: 44 additions & 29 deletions tests/unit/test_util_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,21 +209,25 @@ 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')),
# 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, 0)),
('1.2', (1, 2, 0, 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')),

# We also include a few test cases from the former impl of this class, mainly to note differences in behaviours

# Note that prerelease tags are expected to start with a hyphen or tilde so the expected tag is
# lost in all cases below
('3.55.1.build12', (3, 55, 1, 0, 0)),
('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)),
('2.1.hello', (2, 1, 0, 0, 0))
]

for str_version, expected_result in versions:
print(str_version)
v = Version(str_version)
self.assertEqual(str_version, str(v))
self.assertEqual(v.major, expected_result[0])
Expand All @@ -232,9 +236,18 @@ def test_version_parsing(self):
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:
print(v)
with self.assertRaises(ValueError):
Version(v)

def test_version_compare(self):
# just tests a bunch of versions
Expand All @@ -251,41 +264,43 @@ def test_version_compare(self):

# 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.0.0.5-SNAPSHOT') == Version('4.0.0.5-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.0.6-SNAPSHOT') > Version('4.0.0.5-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'))

# 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'))