~ubuntu-core-dev/merge-o-matic/trunk

57 by Scott James Remnant
replace with new-mom source
1
#!/usr/bin/env python
114 by Scott James Remnant
add the GPL 3 to everything
2
# -*- coding: utf-8 -*-
3
# deb/version.py - parse and compare Debian version strings.
4
#
5
# Copyright © 2008 Canonical Ltd.
6
# Author: Scott James Remnant <scott@ubuntu.com>.
7
#
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of version 3 of the GNU General Public License as
10
# published by the Free Software Foundation.
11
#
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
57 by Scott James Remnant
replace with new-mom source
19
20
import re
21
22
23
# Regular expressions make validating things easy
24
valid_epoch = re.compile(r'^[0-9]+$')
25
valid_upstream = re.compile(r'^[A-Za-z0-9+:.~-]*$')
26
valid_revision = re.compile(r'^[A-Za-z0-9+.~]+$')
27
28
# Character comparison table for upstream and revision components
29
cmp_table = "~ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-.:"
30
31
32
class Version(object):
33
    """Debian version number.
34
35
    This class is designed to be reasonably transparent and allow you
36
    to write code like:
37
38
    |   s.version >= '1.100-1'
39
40
    The comparison will be done according to Debian rules, so '1.2' will
41
    compare lower.
42
43
    Properties:
44
      epoch       Epoch
45
      upstream    Upstream version
46
      revision    Debian/local revision
47
    """
48
49
    def __init__(self, ver):
50
        """Parse a string or number into the three components."""
51
        self.epoch = 0
52
        self.upstream = None
53
        self.revision = None
54
55
        ver = str(ver)
56
        if not len(ver):
57
            raise ValueError
58
59
        # Epoch is component before first colon
60
        idx = ver.find(":")
61
        if idx != -1:
62
            self.epoch = ver[:idx]
63
            if not len(self.epoch):
64
                raise ValueError
65
            if not valid_epoch.search(self.epoch):
66
                raise ValueError
67
            ver = ver[idx+1:]
68
69
        # Revision is component after last hyphen
70
        idx = ver.rfind("-")
71
        if idx != -1:
72
            self.revision = ver[idx+1:]
73
            if not len(self.revision):
74
                raise ValueError
75
            if not valid_revision.search(self.revision):
76
                raise ValueError
77
            ver = ver[:idx]
78
79
        # Remaining component is upstream
80
        self.upstream = ver
81
        if not len(self.upstream):
82
            raise ValueError
83
        if not valid_upstream.search(self.upstream):
84
            raise ValueError
85
86
        self.epoch = int(self.epoch)
87
88
    def getWithoutEpoch(self):
89
        """Return the version without the epoch."""
90
        str = self.upstream
91
        if self.revision is not None:
92
            str += "-%s" % (self.revision,)
93
        return str
94
95
    without_epoch = property(getWithoutEpoch)
96
97
    def __str__(self):
98
        """Return the class as a string for printing."""
99
        str = ""
100
        if self.epoch > 0:
101
            str += "%d:" % (self.epoch,)
102
        str += self.upstream
103
        if self.revision is not None:
104
            str += "-%s" % (self.revision,)
105
        return str
106
107
    def __repr__(self):
108
        """Return a debugging representation of the object."""
109
        return "<%s epoch: %d, upstream: %r, revision: %r>" \
110
               % (self.__class__.__name__, self.epoch,
111
                  self.upstream, self.revision)
112
113
    def __cmp__(self, other):
114
        """Compare two Version classes."""
115
        other = Version(other)
116
117
        result = cmp(self.epoch, other.epoch)
118
        if result != 0: return result
119
120
        result = deb_cmp(self.upstream, other.upstream)
121
        if result != 0: return result
122
123
        result = deb_cmp(self.revision or "", other.revision or "")
124
        if result != 0: return result
125
126
        return 0
127
128
129
def strcut(str, idx, accept):
174 by lool at dooz
Merge misc typo fixes from Karl Goetz
130
    """Cut characters from str that are entirely in accept."""
57 by Scott James Remnant
replace with new-mom source
131
    ret = ""
132
    while idx < len(str) and str[idx] in accept:
133
        ret += str[idx]
134
        idx += 1
135
136
    return (ret, idx)
137
138
def deb_order(str, idx):
139
    """Return the comparison order of two characters."""
140
    if idx >= len(str):
141
        return 0
142
    elif str[idx] == "~":
143
        return -1
144
    else:
145
        return cmp_table.index(str[idx])
146
147
def deb_cmp_str(x, y):
148
    """Compare two strings in a deb version."""
149
    idx = 0
150
    while (idx < len(x)) or (idx < len(y)):
151
        result = deb_order(x, idx) - deb_order(y, idx)
152
        if result < 0:
153
            return -1
154
        elif result > 0:
155
            return 1
156
157
        idx += 1
158
159
    return 0
160
161
def deb_cmp(x, y):
162
    """Implement the string comparison outlined by Debian policy."""
163
    x_idx = y_idx = 0
164
    while x_idx < len(x) or y_idx < len(y):
165
        # Compare strings
166
        (x_str, x_idx) = strcut(x, x_idx, cmp_table)
167
        (y_str, y_idx) = strcut(y, y_idx, cmp_table)
168
        result = deb_cmp_str(x_str, y_str)
169
        if result != 0: return result
170
171
        # Compare numbers
172
        (x_str, x_idx) = strcut(x, x_idx, "0123456789")
173
        (y_str, y_idx) = strcut(y, y_idx, "0123456789")
174
        result = cmp(int(x_str or "0"), int(y_str or "0"))
175
        if result != 0: return result
176
177
    return 0