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
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
66 """simple class to handle soft version number has a tuple while
67 correctly printing it as X.Y.Z
68 """
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
81 return '.'.join([str(i) for i in self])
82
83
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]:
110 self.messages[-1][1][-1].append(msg_suite)
111 else:
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
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
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
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
192 if len(words) == 1 and words[0] == '--':
193 expect_sub = False
194 last = self.entry_class()
195 self.add_entry(last)
196
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
202 elif sline and last is None:
203 self.title = '%s%s' % (self.title, line)
204
205 elif sline and sline[0] == BULLET:
206 expect_sub = False
207 last.add_message(sline[1:].strip())
208
209 elif expect_sub and sline and sline[0] == SUBBULLET:
210 last.add_sub_message(sline[1:].strip())
211
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
221
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