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 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
69 """simple class to handle soft version number has a tuple while
70 correctly printing it as X.Y.Z
71 """
73 if isinstance(versionstr, string_types):
74 versionstr = versionstr.strip(' :')
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
90 return '.'.join([str(i) for i in self])
91
92
93
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]:
121 self.messages[-1][1][-1].append(msg_suite)
122 else:
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
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
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
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
208 if len(words) == 1 and words[0] == '--':
209 expect_sub = False
210 last = self.entry_class()
211 self.add_entry(last)
212
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
218 elif sline and last is None:
219 self.title = '%s%s' % (self.title, line)
220
221 elif sline and sline[0] == BULLET:
222 expect_sub = False
223 last.add_message(sline[1:].strip())
224
225 elif expect_sub and sline and sline[0] == SUBBULLET:
226 last.add_sub_message(sline[1:].strip())
227
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
237
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