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   
 48   
 49  """ 
 50  __docformat__ = "restructuredtext en" 
 51   
 52  import sys 
 53  from stat import S_IWRITE 
 54   
 55  BULLET = '*' 
 56  SUBBULLET = '-' 
 57  INDENT = ' ' * 4 
 58   
59 -class NoEntry(Exception):
60 """raised when we are unable to find an entry"""
61
62 -class EntryNotFound(Exception):
63 """raised when we are unable to find a given entry"""
64
65 -class Version(tuple):
66 """simple class to handle soft version number has a tuple while 67 correctly printing it as X.Y.Z 68 """
69 - def __new__(klass, versionstr):
70 if isinstance(versionstr, basestring): 71 versionstr = versionstr.strip(' :') 72 try: 73 parsed = [int(i) for i in versionstr.split('.')] 74 except ValueError, ex: 75 raise ValueError("invalid literal for version '%s' (%s)"%(versionstr,ex)) 76 else: 77 parsed = versionstr 78 return tuple.__new__(klass, parsed)
79
80 - def __str__(self):
81 return '.'.join([str(i) for i in self])
82 83 # upstream change log ######################################################### 84
85 -class ChangeLogEntry(object):
86 """a change log entry, i.e. a set of messages associated to a version and 87 its release date 88 """ 89 version_class = Version 90
91 - def __init__(self, date=None, version=None, **kwargs):
92 self.__dict__.update(kwargs) 93 if version: 94 self.version = self.version_class(version) 95 else: 96 self.version = None 97 self.date = date 98 self.messages = []
99
100 - def add_message(self, msg):
101 """add a new message""" 102 self.messages.append(([msg],[]))
103
104 - def complete_latest_message(self, msg_suite):
105 """complete the latest added message 106 """ 107 if not self.messages: 108 raise ValueError('unable to complete last message as there is no previous message)') 109 if self.messages[-1][1]: # sub messages 110 self.messages[-1][1][-1].append(msg_suite) 111 else: # message 112 self.messages[-1][0].append(msg_suite)
113
114 - def add_sub_message(self, sub_msg, key=None):
115 if not self.messages: 116 raise ValueError('unable to complete last message as there is no previous message)') 117 if key is None: 118 self.messages[-1][1].append([sub_msg]) 119 else: 120 raise NotImplementedError("sub message to specific key are not implemented yet")
121
122 - def write(self, stream=sys.stdout):
123 """write the entry to file """ 124 stream.write('%s -- %s\n' % (self.date or '', self.version or '')) 125 for msg, sub_msgs in self.messages: 126 stream.write('%s%s %s\n' % (INDENT, BULLET, msg[0])) 127 stream.write(''.join(msg[1:])) 128 if sub_msgs: 129 stream.write('\n') 130 for sub_msg in sub_msgs: 131 stream.write('%s%s %s\n' % (INDENT * 2, SUBBULLET, sub_msg[0])) 132 stream.write(''.join(sub_msg[1:])) 133 stream.write('\n') 134 135 stream.write('\n\n')
136
137 -class ChangeLog(object):
138 """object representation of a whole ChangeLog file""" 139 140 entry_class = ChangeLogEntry 141
142 - def __init__(self, changelog_file, title=''):
143 self.file = changelog_file 144 self.title = title 145 self.additional_content = '' 146 self.entries = [] 147 self.load()
148
149 - def __repr__(self):
150 return '<ChangeLog %s at %s (%s entries)>' % (self.file, id(self), 151 len(self.entries))
152
153 - def add_entry(self, entry):
154 """add a new entry to the change log""" 155 self.entries.append(entry)
156
157 - def get_entry(self, version='', create=None):
158 """ return a given changelog entry 159 if version is omitted, return the current entry 160 """ 161 if not self.entries: 162 if version or not create: 163 raise NoEntry() 164 self.entries.append(self.entry_class()) 165 if not version: 166 if self.entries[0].version and create is not None: 167 self.entries.insert(0, self.entry_class()) 168 return self.entries[0] 169 version = self.version_class(version) 170 for entry in self.entries: 171 if entry.version == version: 172 return entry 173 raise EntryNotFound()
174
175 - def add(self, msg, create=None):
176 """add a new message to the latest opened entry""" 177 entry = self.get_entry(create=create) 178 entry.add_message(msg)
179
180 - def load(self):
181 """ read a logilab's ChangeLog from file """ 182 try: 183 stream = open(self.file) 184 except IOError: 185 return 186 last = None 187 expect_sub = False 188 for line in stream.readlines(): 189 sline = line.strip() 190 words = sline.split() 191 # if new entry 192 if len(words) == 1 and words[0] == '--': 193 expect_sub = False 194 last = self.entry_class() 195 self.add_entry(last) 196 # if old entry 197 elif len(words) == 3 and words[1] == '--': 198 expect_sub = False 199 last = self.entry_class(words[0], words[2]) 200 self.add_entry(last) 201 # if title 202 elif sline and last is None: 203 self.title = '%s%s' % (self.title, line) 204 # if new entry 205 elif sline and sline[0] == BULLET: 206 expect_sub = False 207 last.add_message(sline[1:].strip()) 208 # if new sub_entry 209 elif expect_sub and sline and sline[0] == SUBBULLET: 210 last.add_sub_message(sline[1:].strip()) 211 # if new line for current entry 212 elif sline and last.messages: 213 last.complete_latest_message(line) 214 else: 215 expect_sub = True 216 self.additional_content += line 217 stream.close()
218
219 - def format_title(self):
220 return '%s\n\n' % self.title.strip()
221
222 - def save(self):
223 """write back change log""" 224 # filetutils isn't importable in appengine, so import locally 225 from logilab.common.fileutils import ensure_fs_mode 226 ensure_fs_mode(self.file, S_IWRITE) 227 self.write(open(self.file, 'w'))
228
229 - def write(self, stream=sys.stdout):
230 """write changelog to stream""" 231 stream.write(self.format_title()) 232 for entry in self.entries: 233 entry.write(stream)
234