Skip to content

Commit 95e2517

Browse files
committed
CASSPYTHON-10 Update cassandra.util.Version to better support Cassandra version strings
patch by Bret McGuire; reviewed by Brad Schoening and Bret McGuire
1 parent 32e6f60 commit 95e2517

3 files changed

Lines changed: 104 additions & 111 deletions

File tree

cassandra/metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3290,7 +3290,7 @@ def get_schema_parser(connection, server_version, dse_version, timeout):
32903290
elif v >= Version('6.0.0'):
32913291
return SchemaParserDSE60(connection, timeout)
32923292

3293-
if version >= Version('4-a'):
3293+
if version >= Version('4.0-alpha'):
32943294
return SchemaParserV4(connection, timeout)
32953295
elif version >= Version('3.0.0'):
32963296
return SchemaParserV3(connection, timeout)

cassandra/util.py

Lines changed: 48 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1692,57 +1692,56 @@ def __repr__(self):
16921692
self.lower_bound, self.upper_bound, self.value
16931693
)
16941694

1695+
VERSION_REGEX = re.compile("^(\\d+)\\.(\\d+)(\\.\\d+)?(\\.\\d+)?([~\\-]\\w[.\\w]*(?:-\\w[.\\w]*)*)?(\\+[.\\w]+)?$")
16951696

16961697
@total_ordering
16971698
class Version(object):
16981699
"""
1699-
Internal minimalist class to compare versions.
1700-
A valid version is: <int>.<int>.<int>.<int or str>.
1700+
Representation of a Cassandra version. Mostly follows the implementation of the same logic in the Java driver;
1701+
see https://github.com/apache/cassandra-java-driver/blob/4.19.2/core/src/main/java/com/datastax/oss/driver/api/core/Version.java.
17011702
1702-
TODO: when python2 support is removed, use packaging.version.
1703+
Cassandra versions are assumed to correspond to major.minor.patch with an optional additional numeric build field as well as a
1704+
string prerelease field.
17031705
"""
17041706

1705-
_version = None
1706-
major = None
1707-
minor = 0
1708-
patch = 0
1709-
build = 0
1710-
prerelease = 0
1711-
17121707
def __init__(self, version):
17131708
self._version = version
1714-
if '-' in version:
1715-
version_without_prerelease, self.prerelease = version.split('-', 1)
1716-
else:
1717-
version_without_prerelease = version
1718-
parts = list(reversed(version_without_prerelease.split('.')))
1719-
if len(parts) > 4:
1720-
prerelease_string = "-{}".format(self.prerelease) if self.prerelease else ""
1721-
log.warning("Unrecognized version: {}. Only 4 components plus prerelease are supported. "
1722-
"Assuming version as {}{}".format(version, '.'.join(parts[:-5:-1]), prerelease_string))
1709+
1710+
match = VERSION_REGEX.match(version)
1711+
if not match:
1712+
raise ValueError("Version string {0} did not match expected format".format(version))
1713+
1714+
self.major = int(match[1])
1715+
self.minor = int(match[2])
17231716

17241717
try:
1725-
self.major = int(parts.pop())
1726-
except ValueError as e:
1727-
raise ValueError(
1728-
"Couldn't parse version {}. Version should start with a number".format(version))\
1729-
.with_traceback(e.__traceback__)
1718+
self.patch = self._cleanup_int(match[3])
1719+
except:
1720+
self.patch = 0
1721+
17301722
try:
1731-
self.minor = int(parts.pop()) if parts else 0
1732-
self.patch = int(parts.pop()) if parts else 0
1723+
self.build = self._cleanup_int(match[4])
1724+
except:
1725+
self.build = 0
17331726

1734-
if parts: # we have a build version
1735-
build = parts.pop()
1736-
try:
1737-
self.build = int(build)
1738-
except ValueError:
1739-
self.build = build
1740-
except ValueError:
1741-
assumed_version = "{}.{}.{}.{}-{}".format(self.major, self.minor, self.patch, self.build, self.prerelease)
1742-
log.warning("Unrecognized version {}. Assuming version as {}".format(version, assumed_version))
1727+
try:
1728+
self.prerelease = self._cleanup_str(match[5])
1729+
except:
1730+
self.prerelease = ""
1731+
1732+
# This is used in a few places below so let's just build it now
1733+
self._tuple = (self.major, self.minor, self.patch, self.build, self.prerelease)
1734+
1735+
# Trim off the leading '.' characters and convert the discovered value to an integer
1736+
def _cleanup_int(self, instr):
1737+
return int(instr[1:]) if instr else 0
1738+
1739+
# Trim off the leading '.' or '~' characters and just return the string directly
1740+
def _cleanup_str(self, instr):
1741+
return instr[1:] if instr else ""
17431742

17441743
def __hash__(self):
1745-
return self._version
1744+
return hash(self._tuple)
17461745

17471746
def __repr__(self):
17481747
version_string = "Version({0}, {1}, {2}".format(self.major, self.minor, self.patch)
@@ -1757,48 +1756,27 @@ def __repr__(self):
17571756
def __str__(self):
17581757
return self._version
17591758

1760-
@staticmethod
1761-
def _compare_version_part(version, other_version, cmp):
1762-
if not (isinstance(version, int) and
1763-
isinstance(other_version, int)):
1764-
version = str(version)
1765-
other_version = str(other_version)
1766-
1767-
return cmp(version, other_version)
1768-
1759+
# Methods below leverage left-to-right positional comparison of tuples
17691760
def __eq__(self, other):
17701761
if not isinstance(other, Version):
17711762
return NotImplemented
17721763

1773-
return (self.major == other.major and
1774-
self.minor == other.minor and
1775-
self.patch == other.patch and
1776-
self._compare_version_part(self.build, other.build, lambda s, o: s == o) and
1777-
self._compare_version_part(self.prerelease, other.prerelease, lambda s, o: s == o)
1778-
)
1764+
return self._tuple == other._tuple
17791765

17801766
def __gt__(self, other):
17811767
if not isinstance(other, Version):
17821768
return NotImplemented
17831769

1784-
is_major_ge = self.major >= other.major
1785-
is_minor_ge = self.minor >= other.minor
1786-
is_patch_ge = self.patch >= other.patch
1787-
is_build_gt = self._compare_version_part(self.build, other.build, lambda s, o: s > o)
1788-
is_build_ge = self._compare_version_part(self.build, other.build, lambda s, o: s >= o)
1789-
1790-
# By definition, a prerelease comes BEFORE the actual release, so if a version
1791-
# doesn't have a prerelease, it's automatically greater than anything that does
1792-
if self.prerelease and not other.prerelease:
1793-
is_prerelease_gt = False
1770+
# We start by comparing the first four fields directly
1771+
self_tuple = self._tuple[:4]
1772+
other_tuple = (other.major, other.minor, other.patch, other.build)
1773+
if self_tuple != other_tuple:
1774+
return self_tuple > other_tuple
1775+
# If we're still around we have to check prereleases... prereleases always come before
1776+
# the corresponding version
1777+
elif self.prerelease and not other.prerelease:
1778+
return False
17941779
elif other.prerelease and not self.prerelease:
1795-
is_prerelease_gt = True
1780+
return True
17961781
else:
1797-
is_prerelease_gt = self._compare_version_part(self.prerelease, other.prerelease, lambda s, o: s > o) \
1798-
1799-
return (self.major > other.major or
1800-
(is_major_ge and self.minor > other.minor) or
1801-
(is_major_ge and is_minor_ge and self.patch > other.patch) or
1802-
(is_major_ge and is_minor_ge and is_patch_ge and is_build_gt) or
1803-
(is_major_ge and is_minor_ge and is_patch_ge and is_build_ge and is_prerelease_gt)
1804-
)
1782+
return self.prerelease > other.prerelease

tests/unit/test_util_types.py

Lines changed: 55 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -209,18 +209,13 @@ class VersionTests(unittest.TestCase):
209209

210210
def test_version_parsing(self):
211211
versions = [
212-
('2.0.0', (2, 0, 0, 0, 0)),
213-
('3.1.0', (3, 1, 0, 0, 0)),
214-
('2.4.54', (2, 4, 54, 0, 0)),
215-
('3.1.1.12', (3, 1, 1, 12, 0)),
216-
('3.55.1.build12', (3, 55, 1, 'build12', 0)),
217-
('3.55.1.20190429-TEST', (3, 55, 1, 20190429, 'TEST')),
218-
('4.0-SNAPSHOT', (4, 0, 0, 0, 'SNAPSHOT')),
219-
('1.0.5.4.3', (1, 0, 5, 4, 0)),
220-
('1-SNAPSHOT', (1, 0, 0, 0, 'SNAPSHOT')),
221-
('4.0.1.2.3.4.5-ABC-123-SNAP-TEST.blah', (4, 0, 1, 2, 'ABC-123-SNAP-TEST.blah')),
222-
('2.1.hello', (2, 1, 0, 0, 0)),
223-
('2.test.1', (2, 0, 0, 0, 0)),
212+
# Test cases here adapted from the Java driver cases
213+
# (https://github.com/apache/cassandra-java-driver/blob/4.19.2/core/src/test/java/com/datastax/oss/driver/api/core/VersionTest.java)
214+
('1.2.19', (1, 2, 19, 0, "")),
215+
('1.2', (1, 2, 0, 0, "")),
216+
('1.2-beta1-SNAPSHOT', (1, 2, 0, 0, 'beta1-SNAPSHOT')),
217+
('1.2~beta1-SNAPSHOT', (1, 2, 0, 0, 'beta1-SNAPSHOT')),
218+
('1.2.19.2-SNAPSHOT', (1, 2, 19, 2, 'SNAPSHOT')),
224219
]
225220

226221
for str_version, expected_result in versions:
@@ -232,9 +227,17 @@ def test_version_parsing(self):
232227
self.assertEqual(v.build, expected_result[3])
233228
self.assertEqual(v.prerelease, expected_result[4])
234229

235-
# not supported version formats
236-
with self.assertRaises(ValueError):
237-
Version('test.1.0')
230+
# Note that a few of these formats used to be supported when this class was based on the Python versioning scheme.
231+
# This has been updated to more directly correspond to the Cassandra versioning scheme. See CASSPYTHON-10 for more
232+
# detail.
233+
unsupported_versions = [
234+
"test.1.0",
235+
'2.test.1'
236+
]
237+
238+
for v in unsupported_versions:
239+
with self.assertRaises(ValueError):
240+
Version(v)
238241

239242
def test_version_compare(self):
240243
# just tests a bunch of versions
@@ -251,41 +254,53 @@ def test_version_compare(self):
251254

252255
# patch wins
253256
self.assertTrue(Version('2.3.1') > Version('2.3.0'))
254-
self.assertTrue(Version('2.3.1') > Version('2.3.0.4post0'))
257+
self.assertTrue(Version('2.3.1') > Version('2.3.0-4post0'))
255258
self.assertTrue(Version('2.3.1') > Version('2.3.0.44'))
256259

257260
# various
258261
self.assertTrue(Version('2.3.0.1') > Version('2.3.0.0'))
259262
self.assertTrue(Version('2.3.0.680') > Version('2.3.0.670'))
260263
self.assertTrue(Version('2.3.0.681') > Version('2.3.0.680'))
261-
self.assertTrue(Version('2.3.0.1build0') > Version('2.3.0.1')) # 4th part fallback to str cmp
262-
self.assertTrue(Version('2.3.0.build0') > Version('2.3.0.1')) # 4th part fallback to str cmp
263-
self.assertTrue(Version('2.3.0') < Version('2.3.0.build'))
264-
265-
self.assertTrue(Version('4-a') <= Version('4.0.0'))
266-
self.assertTrue(Version('4-a') <= Version('4.0-alpha1'))
267-
self.assertTrue(Version('4-a') <= Version('4.0-beta1'))
268-
self.assertTrue(Version('4.0.0') >= Version('4.0.0'))
269-
self.assertTrue(Version('4.0.0.421') >= Version('4.0.0'))
270-
self.assertTrue(Version('4.0.1') >= Version('4.0.0'))
264+
265+
# If builds are equal then a prerelease always comes before
266+
self.assertTrue(Version('2.3.0.1-SNAPSHOT') < Version('2.3.0.1'))
267+
268+
# If both have prereleases we fall back to a string compare
269+
self.assertTrue(Version('2.3.0.1-SNAPSHOT') < Version('2.3.0.1-ZNAPSHOT'))
270+
271271
self.assertTrue(Version('2.3.0') == Version('2.3.0'))
272272
self.assertTrue(Version('2.3.32') == Version('2.3.32'))
273273
self.assertTrue(Version('2.3.32') == Version('2.3.32.0'))
274-
self.assertTrue(Version('2.3.0.build') == Version('2.3.0.build'))
274+
self.assertTrue(Version('2.3.0-SNAPSHOT') == Version('2.3.0-SNAPSHOT'))
275275

276-
self.assertTrue(Version('4') == Version('4.0.0'))
277276
self.assertTrue(Version('4.0') == Version('4.0.0.0'))
278277
self.assertTrue(Version('4.0') > Version('3.9.3'))
279278

280-
self.assertTrue(Version('4.0') > Version('4.0-SNAPSHOT'))
281-
self.assertTrue(Version('4.0-SNAPSHOT') == Version('4.0-SNAPSHOT'))
282-
self.assertTrue(Version('4.0.0-SNAPSHOT') == Version('4.0-SNAPSHOT'))
283-
self.assertTrue(Version('4.0.0-SNAPSHOT') == Version('4.0.0-SNAPSHOT'))
284-
self.assertTrue(Version('4.0.0.build5-SNAPSHOT') == Version('4.0.0.build5-SNAPSHOT'))
285-
self.assertTrue(Version('4.1-SNAPSHOT') > Version('4.0-SNAPSHOT'))
286-
self.assertTrue(Version('4.0.1-SNAPSHOT') > Version('4.0.0-SNAPSHOT'))
287-
self.assertTrue(Version('4.0.0.build6-SNAPSHOT') > Version('4.0.0.build5-SNAPSHOT'))
288-
self.assertTrue(Version('4.0-SNAPSHOT2') > Version('4.0-SNAPSHOT1'))
289-
self.assertTrue(Version('4.0-SNAPSHOT2') > Version('4.0.0-SNAPSHOT1'))
290-
291-
self.assertTrue(Version('4.0.0-alpha1-SNAPSHOT') > Version('4.0.0-SNAPSHOT'))
279+
280+
equal_tuples = [
281+
(Version('4.0-SNAPSHOT'), Version('4.0-SNAPSHOT')),
282+
(Version('4.0.0-SNAPSHOT'), Version('4.0-SNAPSHOT')),
283+
(Version('4.0.0-SNAPSHOT'), Version('4.0.0-SNAPSHOT')),
284+
(Version('4.0.0.5-SNAPSHOT'), Version('4.0.0.5-SNAPSHOT'))
285+
]
286+
for (a,b) in equal_tuples:
287+
self.assertEqual(a, b)
288+
self.assertEqual(hash(a), hash(b))
289+
290+
left_greater_tuples = [
291+
(Version('4.0'), Version('4.0-SNAPSHOT')),
292+
(Version('4.1-SNAPSHOT'), Version('4.0-SNAPSHOT')),
293+
(Version('4.0.1-SNAPSHOT'), Version('4.0.0-SNAPSHOT')),
294+
(Version('4.0.0.6-SNAPSHOT'), Version('4.0.0.5-SNAPSHOT')),
295+
(Version('4.0-SNAPSHOT2'), Version('4.0-SNAPSHOT1')),
296+
(Version('4.0-SNAPSHOT2'), Version('4.0.0-SNAPSHOT1')),
297+
(Version('4.0.0-alpha1-SNAPSHOT'), Version('4.0.0-SNAPSHOT'))
298+
]
299+
for (a,b) in left_greater_tuples:
300+
self.assertGreater(a, b)
301+
302+
# Test the version limit for v4 schema parsing in cassandra.metadata to make sure
303+
# all 4.0.x Cassandra servers are covered
304+
self.assertTrue(Version('4.0-alpha') <= Version('4.0.0'))
305+
self.assertTrue(Version('4.0-alpha') <= Version('4.0-alpha1'))
306+
self.assertTrue(Version('4.0-alpha') <= Version('4.0-beta1'))

0 commit comments

Comments
 (0)