1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
63 """simple class to handle soft version number has a tuple while
64 correctly printing it as X.Y.Z
65 """
67 if isinstance(versionstr, basestring):
68 versionstr = versionstr.strip(' :')
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
83 return '.'.join([str(i) for i in self])
84
85
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]:
112 self.messages[-1][1][-1].append(msg_suite)
113 else:
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
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
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
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
194 if len(words) == 1 and words[0] == '--':
195 expect_sub = False
196 last = self.entry_class()
197 self.add_entry(last)
198
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
204 elif sline and last is None:
205 self.title = '%s%s' % (self.title, line)
206
207 elif sline and sline[0] == BULLET:
208 expect_sub = False
209 last.add_message(sline[1:].strip())
210
211 elif expect_sub and sline and sline[0] == SUBBULLET:
212 last.add_sub_message(sline[1:].strip())
213
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
223
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