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 from six import string_types
53
54 BULLET = '*'
55 SUBBULLET = '-'
56 INDENT = ' ' * 4
57
58 -class NoEntry(Exception):
59 """raised when we are unable to find an entry"""
60
61 -class EntryNotFound(Exception):
62 """raised when we are unable to find a given entry"""
63
65 """simple class to handle soft version number has a tuple while
66 correctly printing it as X.Y.Z
67 """
69 if isinstance(versionstr, string_types):
70 versionstr = versionstr.strip(' :')
71 parsed = cls.parse(versionstr)
72 else:
73 parsed = versionstr
74 return tuple.__new__(cls, parsed)
75
76 @classmethod
77 - def parse(cls, versionstr):
78 versionstr = versionstr.strip(' :')
79 try:
80 return [int(i) for i in versionstr.split('.')]
81 except ValueError as ex:
82 raise ValueError("invalid literal for version '%s' (%s)"%(versionstr, ex))
83
85 return '.'.join([str(i) for i in self])
86
87
88
89 -class ChangeLogEntry(object):
90 """a change log entry, i.e. a set of messages associated to a version and
91 its release date
92 """
93 version_class = Version
94
95 - def __init__(self, date=None, version=None, **kwargs):
96 self.__dict__.update(kwargs)
97 if version:
98 self.version = self.version_class(version)
99 else:
100 self.version = None
101 self.date = date
102 self.messages = []
103
104 - def add_message(self, msg):
105 """add a new message"""
106 self.messages.append(([msg], []))
107
108 - def complete_latest_message(self, msg_suite):
109 """complete the latest added message
110 """
111 if not self.messages:
112 raise ValueError('unable to complete last message as there is no previous message)')
113 if self.messages[-1][1]:
114 self.messages[-1][1][-1].append(msg_suite)
115 else:
116 self.messages[-1][0].append(msg_suite)
117
118 - def add_sub_message(self, sub_msg, key=None):
119 if not self.messages:
120 raise ValueError('unable to complete last message as there is no previous message)')
121 if key is None:
122 self.messages[-1][1].append([sub_msg])
123 else:
124 raise NotImplementedError("sub message to specific key are not implemented yet")
125
126 - def write(self, stream=sys.stdout):
127 """write the entry to file """
128 stream.write('%s -- %s\n' % (self.date or '', self.version or ''))
129 for msg, sub_msgs in self.messages:
130 stream.write('%s%s %s\n' % (INDENT, BULLET, msg[0]))
131 stream.write(''.join(msg[1:]))
132 if sub_msgs:
133 stream.write('\n')
134 for sub_msg in sub_msgs:
135 stream.write('%s%s %s\n' % (INDENT * 2, SUBBULLET, sub_msg[0]))
136 stream.write(''.join(sub_msg[1:]))
137 stream.write('\n')
138
139 stream.write('\n\n')
140
142 """object representation of a whole ChangeLog file"""
143
144 entry_class = ChangeLogEntry
145
146 - def __init__(self, changelog_file, title=''):
147 self.file = changelog_file
148 self.title = title
149 self.additional_content = ''
150 self.entries = []
151 self.load()
152
154 return '<ChangeLog %s at %s (%s entries)>' % (self.file, id(self),
155 len(self.entries))
156
157 - def add_entry(self, entry):
158 """add a new entry to the change log"""
159 self.entries.append(entry)
160
161 - def get_entry(self, version='', create=None):
162 """ return a given changelog entry
163 if version is omitted, return the current entry
164 """
165 if not self.entries:
166 if version or not create:
167 raise NoEntry()
168 self.entries.append(self.entry_class())
169 if not version:
170 if self.entries[0].version and create is not None:
171 self.entries.insert(0, self.entry_class())
172 return self.entries[0]
173 version = self.version_class(version)
174 for entry in self.entries:
175 if entry.version == version:
176 return entry
177 raise EntryNotFound()
178
179 - def add(self, msg, create=None):
180 """add a new message to the latest opened entry"""
181 entry = self.get_entry(create=create)
182 entry.add_message(msg)
183
185 """ read a logilab's ChangeLog from file """
186 try:
187 stream = open(self.file)
188 except IOError:
189 return
190 last = None
191 expect_sub = False
192 for line in stream.readlines():
193 sline = line.strip()
194 words = sline.split()
195
196 if len(words) == 1 and words[0] == '--':
197 expect_sub = False
198 last = self.entry_class()
199 self.add_entry(last)
200
201 elif len(words) == 3 and words[1] == '--':
202 expect_sub = False
203 last = self.entry_class(words[0], words[2])
204 self.add_entry(last)
205
206 elif sline and last is None:
207 self.title = '%s%s' % (self.title, line)
208
209 elif sline and sline[0] == BULLET:
210 expect_sub = False
211 last.add_message(sline[1:].strip())
212
213 elif expect_sub and sline and sline[0] == SUBBULLET:
214 last.add_sub_message(sline[1:].strip())
215
216 elif sline and last.messages:
217 last.complete_latest_message(line)
218 else:
219 expect_sub = True
220 self.additional_content += line
221 stream.close()
222
225
232
233 - def write(self, stream=sys.stdout):
234 """write changelog to stream"""
235 stream.write(self.format_title())
236 for entry in self.entries:
237 entry.write(stream)
238