1""" 2Python helper for Semantic Versioning (http://semver.org/) 3""" 4 5import collections 6import re 7 8 9__version__ = '2.7.8' 10__author__ = 'Kostiantyn Rybnikov' 11__author_email__ = 'k-bx@k-bx.com' 12 13_REGEX = re.compile( 14 r""" 15 ^ 16 (?P<major>(?:0|[1-9][0-9]*)) 17 \. 18 (?P<minor>(?:0|[1-9][0-9]*)) 19 \. 20 (?P<patch>(?:0|[1-9][0-9]*)) 21 (\-(?P<prerelease> 22 (?:0|[1-9A-Za-z-][0-9A-Za-z-]*) 23 (\.(?:0|[1-9A-Za-z-][0-9A-Za-z-]*))* 24 ))? 25 (\+(?P<build> 26 [0-9A-Za-z-]+ 27 (\.[0-9A-Za-z-]+)* 28 ))? 29 $ 30 """, re.VERBOSE) 31 32_LAST_NUMBER = re.compile(r'(?:[^\d]*(\d+)[^\d]*)+') 33 34if not hasattr(__builtins__, 'cmp'): 35 def cmp(a, b): 36 return (a > b) - (a < b) 37 38 39def parse(version): 40 """Parse version to major, minor, patch, pre-release, build parts. 41 42 :param version: version string 43 :return: dictionary with the keys 'build', 'major', 'minor', 'patch', 44 and 'prerelease'. The prerelease or build keys can be None 45 if not provided 46 :rtype: dict 47 """ 48 match = _REGEX.match(version) 49 if match is None: 50 raise ValueError('%s is not valid SemVer string' % version) 51 52 version_parts = match.groupdict() 53 54 version_parts['major'] = int(version_parts['major']) 55 version_parts['minor'] = int(version_parts['minor']) 56 version_parts['patch'] = int(version_parts['patch']) 57 58 return version_parts 59 60 61class VersionInfo(collections.namedtuple( 62 'VersionInfo', 'major minor patch prerelease build')): 63 """ 64 :param int major: version when you make incompatible API changes. 65 :param int minor: version when you add functionality in 66 a backwards-compatible manner. 67 :param int patch: version when you make backwards-compatible bug fixes. 68 :param str prerelease: an optional prerelease string 69 :param str build: an optional build string 70 71 >>> import semver 72 >>> ver = semver.parse('3.4.5-pre.2+build.4') 73 >>> ver 74 {'build': 'build.4', 'major': 3, 'minor': 4, 'patch': 5, 75 'prerelease': 'pre.2'} 76 """ 77 __slots__ = () 78 79 def __eq__(self, other): 80 if not isinstance(other, (VersionInfo, dict)): 81 return NotImplemented 82 return _compare_by_keys(self._asdict(), _to_dict(other)) == 0 83 84 def __ne__(self, other): 85 if not isinstance(other, (VersionInfo, dict)): 86 return NotImplemented 87 return _compare_by_keys(self._asdict(), _to_dict(other)) != 0 88 89 def __lt__(self, other): 90 if not isinstance(other, (VersionInfo, dict)): 91 return NotImplemented 92 return _compare_by_keys(self._asdict(), _to_dict(other)) < 0 93 94 def __le__(self, other): 95 if not isinstance(other, (VersionInfo, dict)): 96 return NotImplemented 97 return _compare_by_keys(self._asdict(), _to_dict(other)) <= 0 98 99 def __gt__(self, other): 100 if not isinstance(other, (VersionInfo, dict)): 101 return NotImplemented 102 return _compare_by_keys(self._asdict(), _to_dict(other)) > 0 103 104 def __ge__(self, other): 105 if not isinstance(other, (VersionInfo, dict)): 106 return NotImplemented 107 return _compare_by_keys(self._asdict(), _to_dict(other)) >= 0 108 109 110def _to_dict(obj): 111 if isinstance(obj, VersionInfo): 112 return obj._asdict() 113 return obj 114 115 116def parse_version_info(version): 117 """Parse version string to a VersionInfo instance. 118 119 :param version: version string 120 :return: a :class:`VersionInfo` instance 121 :rtype: :class:`VersionInfo` 122 """ 123 parts = parse(version) 124 version_info = VersionInfo( 125 parts['major'], parts['minor'], parts['patch'], 126 parts['prerelease'], parts['build']) 127 128 return version_info 129 130 131def _nat_cmp(a, b): 132 def convert(text): 133 return int(text) if re.match('^[0-9]+$', text) else text 134 135 def split_key(key): 136 return [convert(c) for c in key.split('.')] 137 138 def cmp_prerelease_tag(a, b): 139 if isinstance(a, int) and isinstance(b, int): 140 return cmp(a, b) 141 elif isinstance(a, int): 142 return -1 143 elif isinstance(b, int): 144 return 1 145 else: 146 return cmp(a, b) 147 148 a, b = a or '', b or '' 149 a_parts, b_parts = split_key(a), split_key(b) 150 for sub_a, sub_b in zip(a_parts, b_parts): 151 cmp_result = cmp_prerelease_tag(sub_a, sub_b) 152 if cmp_result != 0: 153 return cmp_result 154 else: 155 return cmp(len(a), len(b)) 156 157 158def _compare_by_keys(d1, d2): 159 for key in ['major', 'minor', 'patch']: 160 v = cmp(d1.get(key), d2.get(key)) 161 if v: 162 return v 163 164 rc1, rc2 = d1.get('prerelease'), d2.get('prerelease') 165 rccmp = _nat_cmp(rc1, rc2) 166 167 if not rccmp: 168 return 0 169 if not rc1: 170 return 1 171 elif not rc2: 172 return -1 173 174 return rccmp 175 176 177def compare(ver1, ver2): 178 """Compare two versions 179 180 :param ver1: version string 1 181 :param ver2: version string 2 182 :return: The return value is negative if ver1 < ver2, 183 zero if ver1 == ver2 and strictly positive if ver1 > ver2 184 :rtype: int 185 """ 186 187 v1, v2 = parse(ver1), parse(ver2) 188 189 return _compare_by_keys(v1, v2) 190 191 192def match(version, match_expr): 193 """Compare two versions through a comparison 194 195 :param str version: a version string 196 :param str match_expr: operator and version; valid operators are 197 < smaller than 198 > greater than 199 >= greator or equal than 200 <= smaller or equal than 201 == equal 202 != not equal 203 :return: True if the expression matches the version, otherwise False 204 :rtype: bool 205 """ 206 prefix = match_expr[:2] 207 if prefix in ('>=', '<=', '==', '!='): 208 match_version = match_expr[2:] 209 elif prefix and prefix[0] in ('>', '<'): 210 prefix = prefix[0] 211 match_version = match_expr[1:] 212 else: 213 raise ValueError("match_expr parameter should be in format <op><ver>, " 214 "where <op> is one of " 215 "['<', '>', '==', '<=', '>=', '!=']. " 216 "You provided: %r" % match_expr) 217 218 possibilities_dict = { 219 '>': (1,), 220 '<': (-1,), 221 '==': (0,), 222 '!=': (-1, 1), 223 '>=': (0, 1), 224 '<=': (-1, 0) 225 } 226 227 possibilities = possibilities_dict[prefix] 228 cmp_res = compare(version, match_version) 229 230 return cmp_res in possibilities 231 232 233def max_ver(ver1, ver2): 234 """Returns the greater version of two versions 235 236 :param ver1: version string 1 237 :param ver2: version string 2 238 :return: the greater version of the two 239 :rtype: :class:`VersionInfo` 240 """ 241 cmp_res = compare(ver1, ver2) 242 if cmp_res == 0 or cmp_res == 1: 243 return ver1 244 else: 245 return ver2 246 247 248def min_ver(ver1, ver2): 249 """Returns the smaller version of two versions 250 251 :param ver1: version string 1 252 :param ver2: version string 2 253 :return: the smaller version of the two 254 :rtype: :class:`VersionInfo` 255 """ 256 cmp_res = compare(ver1, ver2) 257 if cmp_res == 0 or cmp_res == -1: 258 return ver1 259 else: 260 return ver2 261 262 263def format_version(major, minor, patch, prerelease=None, build=None): 264 """Format a version according to the Semantic Versioning specification 265 266 :param str major: the required major part of a version 267 :param str minor: the required minor part of a version 268 :param str patch: the required patch part of a version 269 :param str prerelease: the optional prerelease part of a version 270 :param str build: the optional build part of a version 271 :return: the formatted string 272 :rtype: str 273 """ 274 version = "%d.%d.%d" % (major, minor, patch) 275 if prerelease is not None: 276 version = version + "-%s" % prerelease 277 278 if build is not None: 279 version = version + "+%s" % build 280 281 return version 282 283 284def _increment_string(string): 285 """ 286 Look for the last sequence of number(s) in a string and increment, from: 287 http://code.activestate.com/recipes/442460-increment-numbers-in-a-string/#c1 288 """ 289 match = _LAST_NUMBER.search(string) 290 if match: 291 next_ = str(int(match.group(1)) + 1) 292 start, end = match.span(1) 293 string = string[:max(end - len(next_), start)] + next_ + string[end:] 294 return string 295 296 297def bump_major(version): 298 """Raise the major part of the version 299 300 :param: version string 301 :return: the raised version string 302 :rtype: str 303 """ 304 verinfo = parse(version) 305 return format_version(verinfo['major'] + 1, 0, 0) 306 307 308def bump_minor(version): 309 """Raise the minor part of the version 310 311 :param: version string 312 :return: the raised version string 313 :rtype: str 314 """ 315 verinfo = parse(version) 316 return format_version(verinfo['major'], verinfo['minor'] + 1, 0) 317 318 319def bump_patch(version): 320 """Raise the patch part of the version 321 322 :param: version string 323 :return: the raised version string 324 :rtype: str 325 """ 326 verinfo = parse(version) 327 return format_version(verinfo['major'], verinfo['minor'], 328 verinfo['patch'] + 1) 329 330 331def bump_prerelease(version, token='rc'): 332 """Raise the prerelease part of the version 333 334 :param version: version string 335 :param token: defaults to 'rc' 336 :return: the raised version string 337 :rtype: str 338 """ 339 verinfo = parse(version) 340 verinfo['prerelease'] = _increment_string( 341 verinfo['prerelease'] or (token or 'rc') + '.0' 342 ) 343 return format_version(verinfo['major'], verinfo['minor'], verinfo['patch'], 344 verinfo['prerelease']) 345 346 347def bump_build(version, token='build'): 348 """Raise the build part of the version 349 350 :param version: version string 351 :param token: defaults to 'build' 352 :return: the raised version string 353 :rtype: str 354 """ 355 verinfo = parse(version) 356 verinfo['build'] = _increment_string( 357 verinfo['build'] or (token or 'build') + '.0' 358 ) 359 return format_version(verinfo['major'], verinfo['minor'], verinfo['patch'], 360 verinfo['prerelease'], verinfo['build']) 361