| 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)
|
| +
|
|
|