| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- # 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)
|