Package logilab-common-0 :: Package 39 :: Package 0 :: Module changelog
[frames] | no frames]

Source Code for Module logilab-common-0.39.0.changelog

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