semver.py 4.7 KB

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