semver.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. # -*- coding: utf-8 -*-
  2. import re
  3. _REGEX = re.compile('^(?P<major>(?:0|[1-9][0-9]*))'
  4. '\.(?P<minor>(?:0|[1-9][0-9]*))'
  5. '\.(?P<patch>(?:0|[1-9][0-9]*))'
  6. '(\-(?P<prerelease>[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?'
  7. '(\+(?P<build>[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$')
  8. _LAST_NUMBER = re.compile(r'(?:[^\d]*(\d+)[^\d]*)+')
  9. if not hasattr(__builtins__, 'cmp'):
  10. cmp = lambda a, b: (a > b) - (a < b)
  11. def parse(version):
  12. """
  13. Parse version to major, minor, patch, pre-release, build parts.
  14. """
  15. match = _REGEX.match(version)
  16. if match is None:
  17. raise ValueError('%s is not valid SemVer string' % version)
  18. verinfo = match.groupdict()
  19. verinfo['major'] = int(verinfo['major'])
  20. verinfo['minor'] = int(verinfo['minor'])
  21. verinfo['patch'] = int(verinfo['patch'])
  22. return verinfo
  23. def compare(ver1, ver2):
  24. def nat_cmp(a, b):
  25. a, b = a or '', b or ''
  26. convert = lambda text: (2, int(text)) if re.match('[0-9]+', text) else (1, text)
  27. split_key = lambda key: [convert(c) for c in key.split('.')]
  28. return cmp(split_key(a), split_key(b))
  29. def compare_by_keys(d1, d2):
  30. for key in ['major', 'minor', 'patch']:
  31. v = cmp(d1.get(key), d2.get(key))
  32. if v:
  33. return v
  34. rc1, rc2 = d1.get('prerelease'), d2.get('prerelease')
  35. rccmp = nat_cmp(rc1, rc2)
  36. build_1, build_2 = d1.get('build'), d2.get('build')
  37. build_cmp = nat_cmp(build_1, build_2)
  38. if not rccmp and not build_cmp:
  39. return 0
  40. if not rc1 and not build_1:
  41. return 1
  42. elif not rc2 and not build_2:
  43. return -1
  44. return rccmp or build_cmp
  45. v1, v2 = parse(ver1), parse(ver2)
  46. return compare_by_keys(v1, v2)
  47. def match(version, match_expr):
  48. prefix = match_expr[:2]
  49. if prefix in ('>=', '<=', '=='):
  50. match_version = match_expr[2:]
  51. elif prefix and prefix[0] in ('>', '<', '='):
  52. prefix = prefix[0]
  53. match_version = match_expr[1:]
  54. else:
  55. raise ValueError("match_expr parameter should be in format <op><ver>, "
  56. "where <op> is one of ['<', '>', '==', '<=', '>=']. "
  57. "You provided: %r" % match_expr)
  58. possibilities_dict = {
  59. '>': (1,),
  60. '<': (-1,),
  61. '==': (0,),
  62. '>=': (0, 1),
  63. '<=': (-1, 0)
  64. }
  65. possibilities = possibilities_dict[prefix]
  66. cmp_res = compare(version, match_version)
  67. return cmp_res in possibilities
  68. def max_ver(ver1, ver2):
  69. cmp_res = compare(ver1, ver2)
  70. if cmp_res == 0 or cmp_res == 1:
  71. return ver1
  72. else:
  73. return ver2
  74. def min_ver(ver1, ver2):
  75. cmp_res = compare(ver1, ver2)
  76. if cmp_res == 0 or cmp_res == -1:
  77. return ver1
  78. else:
  79. return ver2
  80. def format_version(major, minor, patch, prerelease=None, build=None):
  81. version = "%d.%d.%d" % (major, minor, patch)
  82. if prerelease is not None:
  83. version = version + "-%s" % prerelease
  84. if build is not None:
  85. version = version + "+%s" % build
  86. return version
  87. def _increment_string(string):
  88. # look for the last sequence of number(s) in a string and increment, from:
  89. # http://code.activestate.com/recipes/442460-increment-numbers-in-a-string/#c1
  90. match = _LAST_NUMBER.search(string)
  91. if match:
  92. next_ = str(int(match.group(1))+1)
  93. start, end = match.span(1)
  94. string = string[:max(end - len(next_), start)] + next_ + string[end:]
  95. return string
  96. def bump_major(version):
  97. verinfo = parse(version)
  98. return format_version(verinfo['major'] + 1, 0, 0)
  99. def bump_minor(version):
  100. verinfo = parse(version)
  101. return format_version(verinfo['major'], verinfo['minor'] + 1, 0)
  102. def bump_patch(version):
  103. verinfo = parse(version)
  104. return format_version(verinfo['major'], verinfo['minor'], verinfo['patch'] + 1)
  105. def bump_prerelease(version):
  106. verinfo = parse(version)
  107. verinfo['prerelease'] = _increment_string(verinfo['prerelease'] or 'rc.0')
  108. return format_version(verinfo['major'], verinfo['minor'], verinfo['patch'],
  109. verinfo['prerelease'])
  110. def bump_build(version):
  111. verinfo = parse(version)
  112. verinfo['build'] = _increment_string(verinfo['build'] or 'build.0')
  113. return format_version(verinfo['major'], verinfo['minor'], verinfo['patch'],
  114. verinfo['prerelease'], verinfo['build'])