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 and/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) any
      9  # 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 along
     17  # 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
     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
     64 -class Version(tuple):
     65      """simple class to handle soft version number has a tuple while
     66      correctly printing it as X.Y.Z
     67      """
     68 -    def __new__(cls, versionstr):
     69          if isinstance(versionstr, string_types):
     70              versionstr = versionstr.strip(' :') # XXX (syt) duh?
     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
     84 -    def __str__(self):
     85          return '.'.join([str(i) for i in self])
     86
     87  # upstream change log #########################################################
     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]: # sub messages
    114              self.messages[-1][1][-1].append(msg_suite)
    115          else: # message
    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
    141 -class ChangeLog(object):
    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
    153 -    def __repr__(self):
    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
    184 -    def load(self):
    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              # if new entry
    196              if len(words) == 1 and words[0] == '--':
    197                  expect_sub = False
    198                  last = self.entry_class()
    199                  self.add_entry(last)
    200              # if old entry
    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              # if title
    206              elif sline and last is None:
    207                  self.title = '%s%s' % (self.title, line)
    208              # if new entry
    209              elif sline and sline[0] == BULLET:
    210                  expect_sub = False
    211                  last.add_message(sline[1:].strip())
    212              # if new sub_entry
    213              elif expect_sub and sline and sline[0] == SUBBULLET:
    214                  last.add_sub_message(sline[1:].strip())
    215              # if new line for current entry
    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
    223 -    def format_title(self):
    224          return '%s\n\n' % self.title.strip()
    225
    226 -    def save(self):
    227          """write back change log"""
    228          # filetutils isn't importable in appengine, so import locally
    229          from logilab.common.fileutils import ensure_fs_mode
    230          ensure_fs_mode(self.file, S_IWRITE)
    231          self.write(open(self.file, 'w'))
    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