Package logilab :: Package common :: Module changelog

[frames] | no frames]

# Source Code for Module logilab.common.changelog

      1  # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
      2  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
      3  #
      4  # This file is part of logilab-common.
      5  #
      6  # logilab-common is free software: you can redistribute it or modify it under
      7  # the terms of the GNU Lesser General Public License as published by the Free
      8  # Software Foundation, either version 2.1 of the License, or (at your option)
      9  # any later version.
     10  #
     11  # logilab-common is distributed in the hope that it will be useful, but WITHOUT
     12  # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
     13  # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
     14  # details.
     15  #
     16  # You should have received a copy of the GNU Lesser General Public License
     17  # along with logilab-common.  If not, see <http://www.gnu.org/licenses/>.
     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
     67
     68 -class Version(tuple):
     69      """simple class to handle soft version number has a tuple while
     70      correctly printing it as X.Y.Z
     71      """
     72 -    def __new__(cls, versionstr):
     73          if isinstance(versionstr, string_types):
     74              versionstr = versionstr.strip(' :')  # XXX (syt) duh?
     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
     89 -    def __str__(self):
     90          return '.'.join([str(i) for i in self])
     91
     92
     93  # upstream change log #########################################################
     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]:  # sub messages
    121              self.messages[-1][1][-1].append(msg_suite)
    122          else:  # message
    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
    151
    152 -class ChangeLog(object):
    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
    165 -    def __repr__(self):
    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
    196 -    def load(self):
    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              # if new entry
    208              if len(words) == 1 and words[0] == '--':
    209                  expect_sub = False
    210                  last = self.entry_class()
    211                  self.add_entry(last)
    212              # if old entry
    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              # if title
    218              elif sline and last is None:
    219                  self.title = '%s%s' % (self.title, line)
    220              # if new entry
    221              elif sline and sline[0] == BULLET:
    222                  expect_sub = False
    223                  last.add_message(sline[1:].strip())
    224              # if new sub_entry
    225              elif expect_sub and sline and sline[0] == SUBBULLET:
    226                  last.add_sub_message(sline[1:].strip())
    227              # if new line for current entry
    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
    235 -    def format_title(self):
    236          return u'%s\n\n' % self.title.strip()
    237
    238 -    def save(self):
    239          """write back change log"""
    240          # filetutils isn't importable in appengine, so import locally
    241          from logilab.common.fileutils import ensure_fs_mode
    242          ensure_fs_mode(self.file, S_IWRITE)
    243          self.write(codecs.open(self.file, 'w', encoding='utf-8'))
    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