Package logilab-common-0 ::
Package 39 ::
Package 0 ::
Module 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
49 """simple class to handle soft version number has a tuple while
50 correctly printing it as X.Y.Z
51 """
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
64 return '.'.join([str(i) for i in self])
65
66
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]:
93 self.messages[-1][1][-1].append(msg_suite)
94 else:
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
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
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
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
175 if len(words) == 1 and words[0] == '--':
176 expect_sub = False
177 last = self.entry_class()
178 self.add_entry(last)
179
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
185 elif sline and last is None:
186 self.title = '%s%s' % (self.title, line)
187
188 elif sline and sline[0] == BULLET:
189 expect_sub = False
190 last.add_message(sline[1:].strip())
191
192 elif expect_sub and sline and sline[0] == SUBBULLET:
193 last.add_sub_message(sline[1:].strip())
194
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
204
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