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