Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
  • 16 commits
  • 2 files changed
  • 0 commit comments
  • 1 contributor
Showing with 91 additions and 37 deletions.
  1. +46 −31 semver.py
  2. +45 −6 tests/semver_test.py
View
77 semver.py
@@ -2,67 +2,82 @@
import re
+_REGEX = re.compile('^(?P<major>[0-9]+)'
+ '\.(?P<minor>[0-9]+)'
+ '\.(?P<patch>[0-9]+)'
+ '(\-(?P<prerelease>[0-9A-Za-z]+(\.[0-9A-Za-z]+)*))?'
+ '(\+(?P<build>[0-9A-Za-z]+(\.[0-9A-Za-z]+)*))?$')
+
+if 'cmp' not in __builtins__:
+ cmp = lambda a,b: (a > b) - (a < b)
def parse(version):
"""
Parse version to major, minor, patch, pre-release, build parts.
"""
- regex = re.compile(r"^([0-9]+)" # major
- + r"\.([0-9]+)" # minor
- + r"\.([0-9]+)" # patch
- + r"(\-[0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?" # pre-release
- + r"(\+[0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?$") # build
- match = regex.match(version)
+ match = _REGEX.match(version)
if match is None:
- raise ValueError('{0} is not valid SemVer string'.format(version))
- rv = {}
- rv['major'] = int(match.group(1))
- rv['minor'] = int(match.group(2))
- rv['patch'] = int(match.group(3))
- if match.group(4):
- rv['prerelease'] = match.group(4).lstrip('-')
+ raise ValueError('%s is not valid SemVer string' % version)
+
+ verinfo = match.groupdict()
+
+ verinfo['major'] = int(verinfo['major'])
+ verinfo['minor'] = int(verinfo['minor'])
+ verinfo['patch'] = int(verinfo['patch'])
- if match.group(6):
- rv['build'] = match.group(6).lstrip('+')
- return rv
+ return verinfo
def compare(ver1, ver2):
- def compare_by_keys(d1, d2, keys):
- for key in keys:
+ def nat_cmp(a, b):
+ a, b = a or '', b or ''
+ convert = lambda text: text.isdigit() and int(text) or text.lower()
+ alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
+ return cmp(alphanum_key(a), alphanum_key(b))
+
+ def compare_by_keys(d1, d2):
+ for key in ['major', 'minor', 'patch']:
v = cmp(d1.get(key), d2.get(key))
- if v != 0:
+ if v:
return v
- return 0
+ rc1, rc2 = d1.get('prerelease'), d2.get('prerelease')
+ build1, build2 = d1.get('build'), d2.get('build')
+ rccmp = nat_cmp(rc1, rc2)
+ buildcmp = nat_cmp(build1, build2)
+ if not (rc1 or rc2):
+ return buildcmp
+ elif not rc1:
+ return 1
+ elif not rc2:
+ return -1
+ return rccmp or buildcmp or 0
v1, v2 = parse(ver1), parse(ver2)
- return compare_by_keys(
- v1, v2, ['major', 'minor', 'patch', 'prerelease', 'build'])
+ return compare_by_keys(v1, v2)
def match(version, match_expr):
prefix = match_expr[:2]
if prefix in ('>=', '<=', '=='):
match_version = match_expr[2:]
- elif prefix[0] in ('>', '<', '='):
+ elif prefix and prefix[0] in ('>', '<', '='):
prefix = prefix[0]
match_version = match_expr[1:]
else:
- raise ValueError(u"match_expr parameter should be in format <op><ver>, "
- u"where <op> is one of ['<', '>', '==', '<=', '>=']. "
- u"You provided: {}".format(match_expr))
+ raise ValueError("match_expr parameter should be in format <op><ver>, "
+ "where <op> is one of ['<', '>', '==', '<=', '>=']. "
+ "You provided: %r" % match_expr)
possibilities_dict = {
'>': (1,),
'<': (-1,),
'==': (0,),
'>=': (0, 1),
- '<=': (-1, 0)}
+ '<=': (-1, 0)
+ }
+
possibilities = possibilities_dict[prefix]
cmp_res = compare(version, match_version)
- if cmp_res in possibilities:
- return True
- else:
- return False
+ return cmp_res in possibilities
View
51 tests/semver_test.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
+import unittest
from unittest import TestCase
-
from semver import compare
from semver import match
from semver import parse
@@ -9,7 +9,7 @@
class TestSemver(TestCase):
def test_should_parse_version(self):
- self.assertEquals(
+ self.assertEqual(
parse("1.2.3-alpha.1.2+build.11.e0f985a"),
{'major': 1,
'minor': 2,
@@ -18,21 +18,60 @@ def test_should_parse_version(self):
'build': 'build.11.e0f985a'})
def test_should_get_less(self):
- self.assertEquals(
+ self.assertEqual(
compare("1.0.0", "2.0.0"),
-1)
def test_should_get_greater(self):
- self.assertEquals(
+ self.assertEqual(
compare("2.0.0", "1.0.0"),
1)
def test_should_match_simple(self):
- self.assertEquals(
+ self.assertEqual(
match("2.3.7", ">=2.3.6"),
True)
def test_should_no_match_simple(self):
- self.assertEquals(
+ self.assertEqual(
match("2.3.7", ">=2.3.8"),
False)
+
+ def test_should_raise_value_error_for_invalid_value(self):
+ self.assertRaises(ValueError, compare, 'foo', 'bar')
+ self.assertRaises(ValueError, compare, '1.0', '1.0.0')
+ self.assertRaises(ValueError, compare, '1.x', '1.0.0')
+
+ def test_should_raise_value_error_for_invalid_match_expression(self):
+ self.assertRaises(ValueError, match, '1.0.0', '')
+ self.assertRaises(ValueError, match, '1.0.0', '!')
+ self.assertRaises(ValueError, match, '1.0.0', '1.0.0')
+
+ def test_should_follow_specification_comparison(self):
+ # produce comparsion chain:
+ # 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta.2 < 1.0.0-beta.11
+ # < 1.0.0-rc.1 < 1.0.0-rc.1+build.1 < 1.0.0 < 1.0.0+0.3.7 < 1.3.7+build
+ # < 1.3.7+build.2.b8f12d7 < 1.3.7+build.11.e0f985a
+ # and in backward too.
+ chain = ['1.0.0-alpha', '1.0.0-alpha.1', '1.0.0-beta.2',
+ '1.0.0-beta.11', '1.0.0-rc.1', '1.0.0-rc.1+build.1',
+ '1.0.0', '1.0.0+0.3.7', '1.3.7+build', '1.3.7+build.2.b8f12d7',
+ '1.3.7+build.11.e0f985a']
+ versions = zip(chain[:-1], chain[1:])
+ for low_version, high_version in versions:
+ self.assertEqual(
+ compare(low_version, high_version), -1,
+ '%s should be lesser than %s' % (low_version, high_version))
+ self.assertEqual(
+ compare(high_version, low_version), 1,
+ '%s should be higher than %s' % (high_version, low_version))
+
+ def test_compare_rc_builds(self):
+ self.assertEqual(compare('1.0.0-beta.2', '1.0.0-beta.11'), -1)
+
+ def test_compare_release_candidate_with_release(self):
+ self.assertEqual(compare('1.0.0-rc.1+build.1', '1.0.0'), -1)
+
+
+if __name__ == '__main__':
+ unittest.main()

No commit comments for this range

Something went wrong with that request. Please try again.