Attachment 'verlib.py'

Download

   1 r"""
   2 "Rational" version definition and parsing for DistutilsVersionFight
   3 discussion at PyCon 2009.
   4 
   5 >>> from verlib import RationalVersion as V
   6 >>> v = V('1.2.3')
   7 >>> str(v)
   8 '1.2.3'
   9 
  10 >>> v = V('1.2')
  11 >>> str(v)
  12 '1.2'
  13 
  14 >>> v = V('1.2.3a4')
  15 >>> str(v)
  16 '1.2.3a4'
  17 
  18 >>> v = V('1.2c4')
  19 >>> str(v)
  20 '1.2c4'
  21 
  22 >>> v = V('1.2.3.4')
  23 >>> str(v)
  24 '1.2.3.4'
  25 
  26 >>> v = V('1.2.3.4.0b3')
  27 >>> str(v)
  28 '1.2.3.4b3'
  29 
  30 >>> V('1.2.0.0.0') == V('1.2')
  31 True
  32 
  33 # Irrational version strings
  34 
  35 >>> v = V('1')
  36 Traceback (most recent call last):
  37   ...
  38 IrrationalVersionError: 1
  39 >>> v = V('1.2a')
  40 Traceback (most recent call last):
  41   ...
  42 IrrationalVersionError: 1.2a
  43 >>> v = V('1.2.3b')
  44 Traceback (most recent call last):
  45   ...
  46 IrrationalVersionError: 1.2.3b
  47 >>> v = V('1.02')
  48 Traceback (most recent call last):
  49   ...
  50 IrrationalVersionError: cannot have leading zero in version number segment: '02' in '1.02'
  51 >>> v = V('1.2a03')
  52 Traceback (most recent call last):
  53   ...
  54 IrrationalVersionError: cannot have leading zero in version number segment: '03' in '1.2a03'
  55 >>> v = V('1.2a3.04')
  56 Traceback (most recent call last):
  57   ...
  58 IrrationalVersionError: cannot have leading zero in version number segment: '04' in '1.2a3.04'
  59 
  60 # Comparison
  61 
  62 >>> V('1.2.0') == '1.2'
  63 Traceback (most recent call last):
  64   ...
  65 TypeError: cannot compare RationalVersion and str
  66 
  67 >>> V('1.2.0') == V('1.2')
  68 True
  69 >>> V('1.2.0') == V('1.2.3')
  70 False
  71 >>> V('1.2.0') < V('1.2.3')
  72 True
  73 >>> (V('1.0') > V('1.0b2'))
  74 True
  75 >>> (V('1.0') > V('1.0c2') > V('1.0c1') > V('1.0b2') > V('1.0b1') 
  76 ...  > V('1.0a2') > V('1.0a1'))
  77 True
  78 >>> (V('1.0.0') > V('1.0.0c2') > V('1.0.0c1') > V('1.0.0b2') > V('1.0.0b1') 
  79 ...  > V('1.0.0a2') > V('1.0.0a1'))
  80 True
  81 
  82 >>> (V('1.0a1')
  83 ...  < V('1.0a2.dev456')
  84 ...  < V('1.0a2')
  85 ...  < V('1.0a2.1.dev456')  # e.g. need to do a quick post release on 1.0a2
  86 ...  < V('1.0a2.1')
  87 ...  < V('1.0b1.dev456')
  88 ...  < V('1.0b2')
  89 ...  < V('1.0c1.dev456')
  90 ...  < V('1.0c1')
  91 ...  < V('1.0.dev456')
  92 ...  < V('1.0')
  93 ...  < V('1.0.post456'))
  94 True
  95 
  96 """
  97 
  98 import sys
  99 import re
 100 from pprint import pprint
 101 
 102 
 103 class IrrationalVersionError(Exception):
 104     """This is an irrational version."""
 105 
 106 
 107 class RationalVersion(object):
 108     """A rational version.
 109     
 110     Good:
 111         1.2         # equivalent to "1.2.0"
 112         1.2.0
 113         1.2a1
 114         1.2.3a2
 115         1.2.3b1
 116         1.2.3c1
 117         1.2.3.4
 118         TODO: fill this out
 119    
 120     Bad:
 121         1           # mininum two numbers
 122         1.2a        # release level must have a release serial
 123         1.2.3b
 124     """
 125     def __init__(self, s):
 126         self._parse(s)
 127     
 128     _version_re = re.compile(r'''
 129         ^
 130         (\d+\.\d+)              # minimum 'N.N'
 131         ((?:\.\d+)*)            # any number of extra '.N' segments
 132         (?:
 133           ([abc])               # 'a'=alpha, 'b'=beta, 'c'=release candidate
 134           (\d+(?:\.\d+)*)
 135         )?
 136         (\.(dev|post)(\d+))?    # pre- (aka development) and post-release tag
 137         $
 138         ''', re.VERBOSE)
 139     # A marker used in the second and third parts of the `info` tuple, for
 140     # versions that don't have those segments, to sort properly. A example
 141     # of versions in sort order ('highest' last):
 142     #   1.0b1           ((1,0), ('b',1), ('f',))
 143     #   1.0.dev345      ((1,0), ('f',),  ('dev', 345))
 144     #   1.0             ((1,0), ('f',),  ('f',))
 145     #   1.0.post345     ((1,0), ('f',),  ('post', 345))
 146     #                           ^        ^
 147     #   'f' < 'b' -------------/         |
 148     #                                    |
 149     #   'dev' < 'f' < 'post' -----------/
 150     # Other letters would do, bug 'f' for 'final' is kind of nice.
 151     _final_marker = ('f',)
 152 
 153     def _parse(self, s):
 154         match = self._version_re.search(s)
 155         if not match:
 156             raise IrrationalVersionError(s)
 157         groups = match.groups()
 158         parts = []
 159         block = self._parse_numdots(groups[0], s, False, 2)
 160         if groups[1]:
 161             block += self._parse_numdots(groups[1][1:], s)
 162         parts.append(tuple(block))
 163         if groups[2]:
 164             block = [groups[2]]
 165             block += self._parse_numdots(groups[3], s, pad_zeros_length=1)
 166             parts.append(tuple(block))
 167         else:
 168             parts.append(self._final_marker)
 169         if groups[4]:
 170             parts.append((groups[5], int(groups[6])))
 171         else:
 172             parts.append(self._final_marker)
 173         self.info = tuple(parts)
 174         #print "_parse(%r) -> %r" % (s, self.info)
 175     
 176     def _parse_numdots(self, s, full_ver_str, drop_trailing_zeros=True,
 177             pad_zeros_length=0):
 178         """Parse 'N.N.N' sequences, return a list of ints.
 179         
 180         @param s {str} 'N.N.N..." sequence to be parsed
 181         @param full_ver_str {str} The full version string from which this
 182             comes. Used for error strings.
 183         @param drop_trailing_zeros {bool} Whether to drop trailing zeros
 184             from the returned list. Default True.
 185         @param pad_zeros_length {int} The length to which to pad the
 186             returned list with zeros, if necessary. Default 0.
 187         """
 188         nums = []
 189         for n in s.split("."):
 190             if len(n) > 1 and n[0] == '0':
 191                 raise IrrationalVersionError("cannot have leading zero in "
 192                     "version number segment: '%s' in %r" % (n, full_ver_str))
 193             nums.append(int(n))
 194         if drop_trailing_zeros:
 195             while nums and nums[-1] == 0:
 196                 nums.pop()
 197         while len(nums) < pad_zeros_length:
 198             nums.append(0)
 199         return nums
 200     
 201     def __cmp__(self, other):
 202         if not isinstance(other, RationalVersion):
 203             raise TypeError("cannot compare %s and %s"
 204                 % (type(self).__name__, type(other).__name__))
 205         return cmp(self.info, other.info)
 206     
 207     def __str__(self):
 208         main, prerel, devpost = self.info
 209         s = '.'.join(str(v) for v in main if v)
 210         if prerel is not self._final_marker:
 211             s += prerel[0]
 212             s += '.'.join(str(v) for v in prerel[1:] if v)
 213         if devpost is not self._final_marker:
 214             s += '.' + ''.join(str(v) for v in prerel[1:] if v)
 215         return s
 216     
 217     def __repr__(self):
 218         return "%s('%s')" % (self.__class__.__name__, self)
 219 
 220     
 221 #---- mainline and test
 222 
 223 def _test():
 224     import doctest
 225     doctest.testmod()
 226 
 227 def _play():
 228     V = RationalVersion
 229     print V('1.0.dev123') < V('1.0.dev456') < V('1.0') < V('1.0.post456') < V('1.0.post789')
 230 
 231 if __name__ == "__main__":
 232     #_play()
 233     _test()

Attached Files

To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.

You are not allowed to attach a file to this page.

Unable to edit the page? See the FrontPage for instructions.