1
2"""Describe and handle version numbers for applications, modules and packages"""
3
4import re
5
6
7__all__ = 'Version',
8
9
10class Version(str):
11    """A major.minor.micro[extraversion] version string that is comparable"""
12
13    # noinspection PyArgumentList
14    def __new__(cls, major, minor, micro, extraversion=None):
15        if major is minor is micro is extraversion is None:
16            instance = str.__new__(cls, 'undefined')
17            instance._version_info = (None, None, None, None, None)
18            return instance
19        try:
20            major, minor, micro = int(major), int(minor), int(micro)
21        except (TypeError, ValueError):
22            raise TypeError('major, minor and micro must be integer numbers')
23        if extraversion is None:
24            instance = str.__new__(cls, '%d.%d.%d' % (major, minor, micro))
25            weight = 0
26        elif isinstance(extraversion, (int, long)):
27            instance = str.__new__(cls, '%d.%d.%d-%d' % (major, minor, micro, extraversion))
28            weight = 0
29        elif isinstance(extraversion, basestring):
30            instance = str.__new__(cls, '%d.%d.%d%s' % (major, minor, micro, extraversion))
31            match = re.match(r'^[-.]?(?P<name>(pre|rc|alpha|beta|))(?P<number>\d+)$', extraversion)
32            if match:
33                weight_map = {'alpha': -40, 'beta': -30, 'pre': -20, 'rc': -10, '': 0}
34                weight = weight_map[match.group('name')]
35                extraversion = int(match.group('number'))
36            else:
37                weight = 0
38                extraversion = extraversion or None
39        else:
40            raise TypeError('extraversion must be a string, integer, long or None')
41        instance._version_info = (major, minor, micro, weight, extraversion)
42        return instance
43
44    def __init__(self, *args, **kw):
45        super(Version, self).__init__()
46
47    @classmethod
48    def parse(cls, value):
49        if isinstance(value, Version):
50            return value
51        elif not isinstance(value, basestring):
52            raise TypeError('value should be a string')
53        if value == 'undefined':
54            return cls(None, None, None)
55        match = re.match(r'^(?P<major>\d+)(\.(?P<minor>\d+))?(\.(?P<micro>\d+))?(?P<extraversion>.*)$', value)
56        if not match:
57            raise ValueError('not a recognized version string')
58        return cls(**match.groupdict(0))
59
60    @property
61    def major(self):
62        return self._version_info[0]
63
64    @property
65    def minor(self):
66        return self._version_info[1]
67
68    @property
69    def micro(self):
70        return self._version_info[2]
71
72    @property
73    def extraversion(self):
74        return self._version_info[4]
75
76    def __repr__(self):
77        major, minor, micro, weight, extraversion = self._version_info
78        if weight is not None and weight < 0:
79            weight_map = {-10: 'rc', -20: 'pre', -30: 'beta', -40: 'alpha'}
80            extraversion = '%s%d' % (weight_map[weight], extraversion)
81        return '%s(major=%r, minor=%r, micro=%r, extraversion=%r)' % (self.__class__.__name__, major, minor, micro, extraversion)
82
83    def __cmp__(self, other):
84        if isinstance(other, Version):
85            return cmp(self._version_info, other._version_info)
86        elif isinstance(other, basestring):
87            return cmp(str(self), other)
88        else:
89            return NotImplemented
90
91    def __le__(self, other):
92        return self.__cmp__(other) <= 0
93
94    def __lt__(self, other):
95        return self.__cmp__(other) < 0
96
97    def __ge__(self, other):
98        return self.__cmp__(other) >= 0
99
100    def __gt__(self, other):
101        return self.__cmp__(other) > 0
102
103    def __eq__(self, other):
104        return self.__cmp__(other) == 0
105
106    def __ne__(self, other):
107        return self.__cmp__(other) != 0
108
109