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

Source Code for Module logilab.common.changelog

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