Index: third_party/logilab/common/changelog.py |
diff --git a/third_party/logilab/common/changelog.py b/third_party/logilab/common/changelog.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..74f512416dc417ebade0069b404d1127f679831c |
--- /dev/null |
+++ b/third_party/logilab/common/changelog.py |
@@ -0,0 +1,236 @@ |
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
+# |
+# This file is part of logilab-common. |
+# |
+# logilab-common is free software: you can redistribute it and/or modify it under |
+# the terms of the GNU Lesser General Public License as published by the Free |
+# Software Foundation, either version 2.1 of the License, or (at your option) any |
+# later version. |
+# |
+# logilab-common is distributed in the hope that it will be useful, but WITHOUT |
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
+# details. |
+# |
+# You should have received a copy of the GNU Lesser General Public License along |
+# with logilab-common. If not, see <http://www.gnu.org/licenses/>. |
+"""Manipulation of upstream change log files. |
+ |
+The upstream change log files format handled is simpler than the one |
+often used such as those generated by the default Emacs changelog mode. |
+ |
+Sample ChangeLog format:: |
+ |
+ Change log for project Yoo |
+ ========================== |
+ |
+ -- |
+ * add a new functionality |
+ |
+ 2002-02-01 -- 0.1.1 |
+ * fix bug #435454 |
+ * fix bug #434356 |
+ |
+ 2002-01-01 -- 0.1 |
+ * initial release |
+ |
+ |
+There is 3 entries in this change log, one for each released version and one |
+for the next version (i.e. the current entry). |
+Each entry contains a set of messages corresponding to changes done in this |
+release. |
+All the non empty lines before the first entry are considered as the change |
+log title. |
+""" |
+ |
+__docformat__ = "restructuredtext en" |
+ |
+import sys |
+from stat import S_IWRITE |
+ |
+BULLET = '*' |
+SUBBULLET = '-' |
+INDENT = ' ' * 4 |
+ |
+class NoEntry(Exception): |
+ """raised when we are unable to find an entry""" |
+ |
+class EntryNotFound(Exception): |
+ """raised when we are unable to find a given entry""" |
+ |
+class Version(tuple): |
+ """simple class to handle soft version number has a tuple while |
+ correctly printing it as X.Y.Z |
+ """ |
+ def __new__(cls, versionstr): |
+ if isinstance(versionstr, basestring): |
+ versionstr = versionstr.strip(' :') # XXX (syt) duh? |
+ parsed = cls.parse(versionstr) |
+ else: |
+ parsed = versionstr |
+ return tuple.__new__(cls, parsed) |
+ |
+ @classmethod |
+ def parse(cls, versionstr): |
+ versionstr = versionstr.strip(' :') |
+ try: |
+ return [int(i) for i in versionstr.split('.')] |
+ except ValueError, ex: |
+ raise ValueError("invalid literal for version '%s' (%s)"%(versionstr, ex)) |
+ |
+ def __str__(self): |
+ return '.'.join([str(i) for i in self]) |
+ |
+# upstream change log ######################################################### |
+ |
+class ChangeLogEntry(object): |
+ """a change log entry, i.e. a set of messages associated to a version and |
+ its release date |
+ """ |
+ version_class = Version |
+ |
+ def __init__(self, date=None, version=None, **kwargs): |
+ self.__dict__.update(kwargs) |
+ if version: |
+ self.version = self.version_class(version) |
+ else: |
+ self.version = None |
+ self.date = date |
+ self.messages = [] |
+ |
+ def add_message(self, msg): |
+ """add a new message""" |
+ self.messages.append(([msg], [])) |
+ |
+ def complete_latest_message(self, msg_suite): |
+ """complete the latest added message |
+ """ |
+ if not self.messages: |
+ raise ValueError('unable to complete last message as there is no previous message)') |
+ if self.messages[-1][1]: # sub messages |
+ self.messages[-1][1][-1].append(msg_suite) |
+ else: # message |
+ self.messages[-1][0].append(msg_suite) |
+ |
+ def add_sub_message(self, sub_msg, key=None): |
+ if not self.messages: |
+ raise ValueError('unable to complete last message as there is no previous message)') |
+ if key is None: |
+ self.messages[-1][1].append([sub_msg]) |
+ else: |
+ raise NotImplementedError("sub message to specific key are not implemented yet") |
+ |
+ def write(self, stream=sys.stdout): |
+ """write the entry to file """ |
+ stream.write('%s -- %s\n' % (self.date or '', self.version or '')) |
+ for msg, sub_msgs in self.messages: |
+ stream.write('%s%s %s\n' % (INDENT, BULLET, msg[0])) |
+ stream.write(''.join(msg[1:])) |
+ if sub_msgs: |
+ stream.write('\n') |
+ for sub_msg in sub_msgs: |
+ stream.write('%s%s %s\n' % (INDENT * 2, SUBBULLET, sub_msg[0])) |
+ stream.write(''.join(sub_msg[1:])) |
+ stream.write('\n') |
+ |
+ stream.write('\n\n') |
+ |
+class ChangeLog(object): |
+ """object representation of a whole ChangeLog file""" |
+ |
+ entry_class = ChangeLogEntry |
+ |
+ def __init__(self, changelog_file, title=''): |
+ self.file = changelog_file |
+ self.title = title |
+ self.additional_content = '' |
+ self.entries = [] |
+ self.load() |
+ |
+ def __repr__(self): |
+ return '<ChangeLog %s at %s (%s entries)>' % (self.file, id(self), |
+ len(self.entries)) |
+ |
+ def add_entry(self, entry): |
+ """add a new entry to the change log""" |
+ self.entries.append(entry) |
+ |
+ def get_entry(self, version='', create=None): |
+ """ return a given changelog entry |
+ if version is omitted, return the current entry |
+ """ |
+ if not self.entries: |
+ if version or not create: |
+ raise NoEntry() |
+ self.entries.append(self.entry_class()) |
+ if not version: |
+ if self.entries[0].version and create is not None: |
+ self.entries.insert(0, self.entry_class()) |
+ return self.entries[0] |
+ version = self.version_class(version) |
+ for entry in self.entries: |
+ if entry.version == version: |
+ return entry |
+ raise EntryNotFound() |
+ |
+ def add(self, msg, create=None): |
+ """add a new message to the latest opened entry""" |
+ entry = self.get_entry(create=create) |
+ entry.add_message(msg) |
+ |
+ def load(self): |
+ """ read a logilab's ChangeLog from file """ |
+ try: |
+ stream = open(self.file) |
+ except IOError: |
+ return |
+ last = None |
+ expect_sub = False |
+ for line in stream.readlines(): |
+ sline = line.strip() |
+ words = sline.split() |
+ # if new entry |
+ if len(words) == 1 and words[0] == '--': |
+ expect_sub = False |
+ last = self.entry_class() |
+ self.add_entry(last) |
+ # if old entry |
+ elif len(words) == 3 and words[1] == '--': |
+ expect_sub = False |
+ last = self.entry_class(words[0], words[2]) |
+ self.add_entry(last) |
+ # if title |
+ elif sline and last is None: |
+ self.title = '%s%s' % (self.title, line) |
+ # if new entry |
+ elif sline and sline[0] == BULLET: |
+ expect_sub = False |
+ last.add_message(sline[1:].strip()) |
+ # if new sub_entry |
+ elif expect_sub and sline and sline[0] == SUBBULLET: |
+ last.add_sub_message(sline[1:].strip()) |
+ # if new line for current entry |
+ elif sline and last.messages: |
+ last.complete_latest_message(line) |
+ else: |
+ expect_sub = True |
+ self.additional_content += line |
+ stream.close() |
+ |
+ def format_title(self): |
+ return '%s\n\n' % self.title.strip() |
+ |
+ def save(self): |
+ """write back change log""" |
+ # filetutils isn't importable in appengine, so import locally |
+ from logilab.common.fileutils import ensure_fs_mode |
+ ensure_fs_mode(self.file, S_IWRITE) |
+ self.write(open(self.file, 'w')) |
+ |
+ def write(self, stream=sys.stdout): |
+ """write changelog to stream""" |
+ stream.write(self.format_title()) |
+ for entry in self.entries: |
+ entry.write(stream) |
+ |