Package logilab :: Package common :: Module changelog
[frames] | no frames]

Source Code for Module logilab.common.changelog

  1  # copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. 
  2  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr 
  3  # 
  4  # This file is part of logilab-common. 
  5  # 
  6  # logilab-common is free software: you can redistribute it and/or modify it under 
  7  # the terms of the GNU Lesser General Public License as published by the Free 
  8  # Software Foundation, either version 2.1 of the License, or (at your option) any 
  9  # later version. 
 10  # 
 11  # logilab-common is distributed in the hope that it will be useful, but WITHOUT 
 12  # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
 13  # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more 
 14  # details. 
 15  # 
 16  # You should have received a copy of the GNU Lesser General Public License along 
 17  # with logilab-common.  If not, see <http://www.gnu.org/licenses/>. 
 18  """Manipulation of upstream change log files. 
 19   
 20  The upstream change log files format handled is simpler than the one 
 21  often used such as those generated by the default Emacs changelog mode. 
 22   
 23  Sample ChangeLog format:: 
 24   
 25    Change log for project Yoo 
 26    ========================== 
 27   
 28     -- 
 29        * add a new functionality 
 30   
 31    2002-02-01 -- 0.1.1 
 32        * fix bug #435454 
 33        * fix bug #434356 
 34   
 35    2002-01-01 -- 0.1 
 36        * initial release 
 37   
 38   
 39  There is 3 entries in this change log, one for each released version and one 
 40  for the next version (i.e. the current entry). 
 41  Each entry contains a set of messages corresponding to changes done in this 
 42  release. 
 43  All the non empty lines before the first entry are considered as the change 
 44  log title. 
 45  """ 
 46   
 47  __docformat__ = "restructuredtext en" 
 48   
 49  import sys 
 50  from stat import S_IWRITE 
 51   
 52  BULLET = '*' 
 53  SUBBULLET = '-' 
 54  INDENT = ' ' * 4 
55 56 -class NoEntry(Exception):
57 """raised when we are unable to find an entry"""
58
59 -class EntryNotFound(Exception):
60 """raised when we are unable to find a given entry"""
61
62 -class Version(tuple):
63 """simple class to handle soft version number has a tuple while 64 correctly printing it as X.Y.Z 65 """
66 - def __new__(cls, versionstr):
67 if isinstance(versionstr, basestring): 68 versionstr = versionstr.strip(' :') # XXX (syt) duh? 69 parsed = cls.parse(versionstr) 70 else: 71 parsed = versionstr 72 return tuple.__new__(cls, parsed)
73 74 @classmethod
75 - def parse(cls, versionstr):
76 versionstr = versionstr.strip(' :') 77 try: 78 return [int(i) for i in versionstr.split('.')] 79 except ValueError, ex: 80 raise ValueError("invalid literal for version '%s' (%s)"%(versionstr, ex))
81
82 - def __str__(self):
83 return '.'.join([str(i) for i in self])
84
85 # upstream change log ######################################################### 86 87 -class ChangeLogEntry(object):
88 """a change log entry, i.e. a set of messages associated to a version and 89 its release date 90 """ 91 version_class = Version 92
93 - def __init__(self, date=None, version=None, **kwargs):
94 self.__dict__.update(kwargs) 95 if version: 96 self.version = self.version_class(version) 97 else: 98 self.version = None 99 self.date = date 100 self.messages = []
101
102 - def add_message(self, msg):
103 """add a new message""" 104 self.messages.append(([msg], []))
105
106 - def complete_latest_message(self, msg_suite):
107 """complete the latest added message 108 """ 109 if not self.messages: 110 raise ValueError('unable to complete last message as there is no previous message)') 111 if self.messages[-1][1]: # sub messages 112 self.messages[-1][1][-1].append(msg_suite) 113 else: # message 114 self.messages[-1][0].append(msg_suite)
115
116 - def add_sub_message(self, sub_msg, key=None):
117 if not self.messages: 118 raise ValueError('unable to complete last message as there is no previous message)') 119 if key is None: 120 self.messages[-1][1].append([sub_msg]) 121 else: 122 raise NotImplementedError("sub message to specific key are not implemented yet")
123
124 - def write(self, stream=sys.stdout):
125 """write the entry to file """ 126 stream.write('%s -- %s\n' % (self.date or '', self.version or '')) 127 for msg, sub_msgs in self.messages: 128 stream.write('%s%s %s\n' % (INDENT, BULLET, msg[0])) 129 stream.write(''.join(msg[1:])) 130 if sub_msgs: 131 stream.write('\n') 132 for sub_msg in sub_msgs: 133 stream.write('%s%s %s\n' % (INDENT * 2, SUBBULLET, sub_msg[0])) 134 stream.write(''.join(sub_msg[1:])) 135 stream.write('\n') 136 137 stream.write('\n\n')
138
139 -class ChangeLog(object):
140 """object representation of a whole ChangeLog file""" 141 142 entry_class = ChangeLogEntry 143
144 - def __init__(self, changelog_file, title=''):
145 self.file = changelog_file 146 self.title = title 147 self.additional_content = '' 148 self.entries = [] 149 self.load()
150
151 - def __repr__(self):
152 return '<ChangeLog %s at %s (%s entries)>' % (self.file, id(self), 153 len(self.entries))
154
155 - def add_entry(self, entry):
156 """add a new entry to the change log""" 157 self.entries.append(entry)
158
159 - def get_entry(self, version='', create=None):
160 """ return a given changelog entry 161 if version is omitted, return the current entry 162 """ 163 if not self.entries: 164 if version or not create: 165 raise NoEntry() 166 self.entries.append(self.entry_class()) 167 if not version: 168 if self.entries[0].version and create is not None: 169 self.entries.insert(0, self.entry_class()) 170 return self.entries[0] 171 version = self.version_class(version) 172 for entry in self.entries: 173 if entry.version == version: 174 return entry 175 raise EntryNotFound()
176
177 - def add(self, msg, create=None):
178 """add a new message to the latest opened entry""" 179 entry = self.get_entry(create=create) 180 entry.add_message(msg)
181
182 - def load(self):
183 """ read a logilab's ChangeLog from file """ 184 try: 185 stream = open(self.file) 186 except IOError: 187 return 188 last = None 189 expect_sub = False 190 for line in stream.readlines(): 191 sline = line.strip() 192 words = sline.split() 193 # if new entry 194 if len(words) == 1 and words[0] == '--': 195 expect_sub = False 196 last = self.entry_class() 197 self.add_entry(last) 198 # if old entry 199 elif len(words) == 3 and words[1] == '--': 200 expect_sub = False 201 last = self.entry_class(words[0], words[2]) 202 self.add_entry(last) 203 # if title 204 elif sline and last is None: 205 self.title = '%s%s' % (self.title, line) 206 # if new entry 207 elif sline and sline[0] == BULLET: 208 expect_sub = False 209 last.add_message(sline[1:].strip()) 210 # if new sub_entry 211 elif expect_sub and sline and sline[0] == SUBBULLET: 212 last.add_sub_message(sline[1:].strip()) 213 # if new line for current entry 214 elif sline and last.messages: 215 last.complete_latest_message(line) 216 else: 217 expect_sub = True 218 self.additional_content += line 219 stream.close()
220
221 - def format_title(self):
222 return '%s\n\n' % self.title.strip()
223
224 - def save(self):
225 """write back change log""" 226 # filetutils isn't importable in appengine, so import locally 227 from logilab.common.fileutils import ensure_fs_mode 228 ensure_fs_mode(self.file, S_IWRITE) 229 self.write(open(self.file, 'w'))
230
231 - def write(self, stream=sys.stdout):
232 """write changelog to stream""" 233 stream.write(self.format_title()) 234 for entry in self.entries: 235 entry.write(stream)
236