[python-debian/master] Overhaul the parsing code to follow that used by dpkg-parsechangelog.py.

James Westby jw+debian at jameswestby.net
Tue Jul 22 17:11:58 UTC 2008


* Overhaul the parsing code to follow that used by dpkg-parsechangelog.py,
  making it much more robust.
  - There is now a "strict" option to turn warnings in to errors. It is
    on by default.
* The file parameter in the changelog can now be a file-like object,
  to save reading a file to a string first. Thanks to Tilman Koschnick.
  (Closes: #487797)
---
 debian/changelog           |    7 +
 debian/copyright           |    1 +
 debian_bundle/changelog.py | 1049 +++++++++++++++++++++++++-------------------
 3 files changed, 609 insertions(+), 448 deletions(-)

diff --git a/debian/changelog b/debian/changelog
index c1067be..75aa718 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -5,6 +5,13 @@ python-debian (0.1.11) UNRELEASED; urgency=low
     fuse's changelog.
   * Allow any number of spaces before "urgency" in the header line of a
     changelog, see lvm2's changelog in Ubuntu.
+  * Overhaul the parsing code to follow that used by dpkg-parsechangelog.py,
+    making it much more robust.
+    - There is now a "strict" option to turn warnings in to errors. It is
+      on by default.
+  * The file parameter in the changelog can now be a file-like object,
+    to save reading a file to a string first. Thanks to Tilman Koschnick.
+    (Closes: #487797)
 
   [ John Wright ]
   * debian/control:
diff --git a/debian/copyright b/debian/copyright
index 14f6c90..7acc0be 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -12,6 +12,7 @@ Copyright:
   changelog.py, setup.py, README.Changelog and the contents of
   examples/changelog/ are
                Copyright (C) 2006-7 James Westby <jw+debian at jameswestby.net>
+               Copyright (C) 2008 Canonical Ltd.
 
   debian_support.py is Copyright (C) 2005 Florian Weimer <fw at deneb.enyo.de>
 
diff --git a/debian_bundle/changelog.py b/debian_bundle/changelog.py
index 9c546b5..ee6be2a 100644
--- a/debian_bundle/changelog.py
+++ b/debian_bundle/changelog.py
@@ -1,5 +1,6 @@
 # changelog.py -- Python module for Debian changelogs
 # Copyright (C) 2006-7 James Westby <jw+debian at jameswestby.net>
+# Copyright (C) 2008 Canonical Ltd.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -15,365 +16,528 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
+# The parsing code is based on that from dpkg which is:
+# Copyright 1996 Ian Jackson
+# Copyright 2005 Frank Lichtenheld <frank at lichtenheld.de>
+# and licensed under the same license as above.
+
 """This module implements facilities to deal with Debian changelogs."""
 
-import os
 import re
 import unittest
+import warnings
 
 import debian_support
 
 class ChangelogParseError(StandardError):
-  """Indicates that the changelog could not be parsed"""
-  is_user_error = True
+    """Indicates that the changelog could not be parsed"""
+    is_user_error = True
 
-  def __init__(self, line):
-    self._line=line
+    def __init__(self, line):
+        self._line=line
 
-  def __str__(self):
-    return "Could not parse changelog: "+self._line
+    def __str__(self):
+        return "Could not parse changelog: "+self._line
 
 class ChangelogCreateError(StandardError):
-  """Indicates that changelog could not be created, as all the information
-  required was not given"""
+    """Indicates that changelog could not be created, as all the information
+    required was not given"""
 
 class VersionError(StandardError):
-  """Indicates that the version does not conform to the required format"""
+    """Indicates that the version does not conform to the required format"""
 
-  is_user_error = True
+    is_user_error = True
 
-  def __init__(self, version):
-    self._version=version
+    def __init__(self, version):
+        self._version=version
 
-  def __str__(self):
-    return "Could not parse version: "+self._version
+    def __str__(self):
+        return "Could not parse version: "+self._version
 
 class Version(debian_support.Version, object):
-  """Represents a version of a Debian package."""
-  # Subclassing debian_support.Version for its rich comparison
-
-  def __init__(self, version):
-    version = str(version)
-    debian_support.Version.__init__(self, version)
-
-    self.full_version = version
+    """Represents a version of a Debian package."""
+    # Subclassing debian_support.Version for its rich comparison
 
-  def __setattr__(self, attr, value):
-    """Update all the attributes, given a particular piece of the version
+    def __init__(self, version):
+        version = str(version)
+        debian_support.Version.__init__(self, version)
 
-    Allowable values for attr, hopefully self-explanatory:
-      full_version
-      epoch
-      upstream_version
-      debian_version
-
-    Any attribute starting with __ is given to object's __setattr__ method.
-    """
+        self.full_version = version
 
-    attrs = ('full_version', 'epoch', 'upstream_version', 'debian_version')
-
-    if attr.startswith('_Version__'):
-      object.__setattr__(self, attr, value)
-      return
-    elif attr not in attrs:
-      raise AttributeError("Cannot assign to attribute " + attr)
-
-    if attr == 'full_version':
-      version = value
-      p = re.compile(r'^(?:(?P<epoch>\d+):)?'
-                     + r'(?P<upstream_version>[A-Za-z0-9.+:~-]+?)'
-                     + r'(?:-(?P<debian_version>[A-Za-z0-9.~+]+))?$')
-      m = p.match(version)
-      if m is None:
-        raise VersionError(version)
-      for key, value in m.groupdict().items():
-        object.__setattr__(self, key, value)
-      self.__asString = version
+    def __setattr__(self, attr, value):
+      """Update all the attributes, given a particular piece of the version
+  
+      Allowable values for attr, hopefully self-explanatory:
+        full_version
+        epoch
+        upstream_version
+        debian_version
+
+      Any attribute starting with __ is given to object's __setattr__ method.
+      """
+
+      attrs = ('full_version', 'epoch', 'upstream_version', 'debian_version')
+
+      if attr.startswith('_Version__'):
+          object.__setattr__(self, attr, value)
+          return
+      elif attr not in attrs:
+          raise AttributeError("Cannot assign to attribute " + attr)
+
+      if attr == 'full_version':
+          version = value
+          p = re.compile(r'^(?:(?P<epoch>\d+):)?'
+                         + r'(?P<upstream_version>[A-Za-z0-9.+:~-]+?)'
+                         + r'(?:-(?P<debian_version>[A-Za-z0-9.~+]+))?$')
+          m = p.match(version)
+          if m is None:
+              raise VersionError(version)
+          for key, value in m.groupdict().items():
+              object.__setattr__(self, key, value)
+          self.__asString = version
     
-    else:
-      # Construct a full version from what was given and pass it back here
-      d = {}
-      for a in attrs[1:]:
-        if a == attr:
-          d[a] = value
-        else:
-          d[a] = getattr(self, a)
-
-      version = ""
-      if d['epoch'] and d['epoch'] != '0':
-        version += d['epoch'] + ":"
-      version += d['upstream_version']
-      if d['debian_version']:
-        version += '-' + d['debian_version']
-
-      self.full_version = version
-
-  full_version = property(lambda self: self.__asString)
+      else:
+          # Construct a full version from what was given and pass it back here
+          d = {}
+          for a in attrs[1:]:
+              if a == attr:
+                  d[a] = value
+              else:
+                  d[a] = getattr(self, a)
+
+          version = ""
+          if d['epoch'] and d['epoch'] != '0':
+              version += d['epoch'] + ":"
+          version += d['upstream_version']
+          if d['debian_version']:
+              version += '-' + d['debian_version']
+
+          self.full_version = version
+
+    full_version = property(lambda self: self.__asString)
 
 class ChangeBlock(object):
-  """Holds all the information about one block from the changelog."""
-
-  def __init__(self, package=None, version=None, distributions=None, 
-                urgency=None, changes=None, author=None, date=None):
-    self.version = version
-    self.package = package
-    self.distributions = distributions
-    self.urgency = urgency
-    self._changes = changes
-    self.author = author
-    self.date = date
-    self._trailing = 0
-
-  def changes(self):
-    return self._changes
-
-  def add_trailing_newline(self):
-    self._trailing += 1
-
-  def del_trailing_newline(self):
-    self._trailing -= 1
-
-  def add_change(self, change):
-    if self._changes is None:
-      self._changes = [change]
-    else:
-#Bit of trickery to keep the formatting nicer with a blank line at the end if there is one
-      changes = self._changes
-      changes.reverse()
-      added = False
-      for i in range(len(changes)):
-        m = blankline.match(changes[i])
-        if m is None:
-          changes.insert(i, change)
-          added = True
-          break
-      changes.reverse()
-      if not added:
-        changes.append(change)
-      self._changes = changes
-
-  def __str__(self):
-    block = ""
-    if self.package is None:
-      raise ChangelogCreateError("Package not specified")
-    block += self.package + " "
-    if self.version is None:
-      raise ChangelogCreateError("Version not specified")
-    block += "(" + str(self.version) + ") "
-    if self.distributions is None:
-      raise ChangelogCreateError("Distribution not specified")
-    block += self.distributions + "; "
-    if self.urgency is None:
-      raise ChangelogCreateError("Urgency not specified")
-    block += "urgency=" + self.urgency + "\n"
-    if self.changes() is None:
-      raise ChangelogCreateError("Changes not specified")
-    for change in self.changes():
-      block += change + "\n"
-    if self.author is None:
-      raise ChangelogCreateError("Author not specified")
-    if self.date is None:
-      raise ChangelogCreateError("Date not specified")
-    block += " -- " + self.author + "  " + self.date + "\n"
-    if self._trailing > 0:
-      for i in range(self._trailing):
-        block += "\n"
-    return block
-
-topline = re.compile('^([a-z0-9][-a-z0-9.+]+) \(([-0-9a-zA-Z.:~+]+)\) '
-      +'([-+.0-9a-zA-Z ]+); *urgency=([a-zA-Z]+)')
-blankline = re.compile('^[ \t]*$')
-change = re.compile('^[ ][ ]+.*$')
-endline = re.compile('^ -- (.*)  (\w\w\w, +(\d| \d|\d\d) \w\w\w \d\d\d\d '+
-      '\d\d:\d\d:\d\d [-+]\d\d\d\d( \(.*\))?)\s*$')
-endline_nodetails = re.compile('^ --(?: (.*)  (\w\w\w, +(\d| \d|\d\d) \w\w\w \d\d\d\d '+
-      '\d\d:\d\d:\d\d [-+]\d\d\d\d( \(.*\))?))?\s*$')
+    """Holds all the information about one block from the changelog."""
+
+    def __init__(self, package=None, version=None, distributions=None,
+                urgency=None, urgency_comment=None, changes=None,
+                author=None, date=None, other_pairs=None):
+        self._raw_version = None
+        self._set_version(version)
+        self.package = package
+        self.distributions = distributions
+        self.urgency = urgency or "unknown"
+        self.urgency_comment = urgency_comment or ''
+        self._changes = changes
+        self.author = author
+        self.date = date
+        self._trailing = []
+        self.other_pairs = other_pairs or {}
+        self._no_trailer = False
+        self._trailer_separator = "  "
+
+    def _set_version(self, version):
+        if version is not None:
+            self._raw_version = str(version)
+
+    def _get_version(self):
+        return Version(self._raw_version)
+
+    version = property(_get_version, _set_version)
+
+    def other_keys_normalised(self):
+        norm_dict = {}
+        for (key, value) in other_pairs.items():
+            key = key[0].upper() + key[1:].lower()
+            m = xbcs_re.match(key)
+            if m is None:
+                key = "XS-%s" % key
+            norm_dict[key] = value
+        return norm_dict
 
-class Changelog(object):
-  """Represents a debian/changelog file. You can ask it several things about
-  the file."""
+    def changes(self):
+        return self._changes
 
+    def add_trailing_line(self, line):
+        self._trailing.append(line)
 
-  def __init__(self, file=None, max_blocks=None, allow_empty_author=False):
-    """Set up the Changelog for use. file is the contects of the changelog.
-    """
-    self._blocks = []
-    if file is not None:
-      try:
-        self.parse_changelog(file, max_blocks, allow_empty_author=allow_empty_author)
-      except ChangelogParseError:
-        pass
-
-
-  def parse_changelog(self, file, max_blocks=None, allow_empty_author=False):
-      before = 1
-      inblock = 2
-
-      package = None
-      version = None
-      distributions = None
-      urgency = None
-      changes = []
-      author = None
-      date = None
-
-      self._file = file
-      state = before
-      for line in self._file.split('\n'):
-        if state == before:
-          m = topline.match(line)
-          if m is not None:
-            state = inblock
-            package = m.group(1)
-            version = Version(m.group(2))
-            distributions = m.group(3)
-            urgency = m.group(4)
-          else:
-            m = blankline.match(line)
-            if m is None:
-              raise ChangelogParseError(line)
-            elif len(self._blocks) > 0:
-              self._blocks[-1].add_trailing_newline()
-        elif state == inblock:
-          m = blankline.match(line)
-          if m is not None:
-            changes.append(line)
-          else:
-            if not allow_empty_author:
-              m = endline.match(line)
-            else:
-              m = endline_nodetails.match(line)
-            if m is not None:
-              state = before
-              author = m.group(1)
-              date = m.group(2)
-              block = ChangeBlock(package, version, distributions, urgency,
-                  changes, author, date)
-              self._blocks.append(block)
-              (package, version, distributions, urgency, author, date) = \
-                  (None, None, None, None, None, None)
-              changes = []
-              if max_blocks is not None and len(self._blocks) >= max_blocks:
-                break
-            else:
-              m = change.match(line)
-              if m is None:
-                raise ChangelogParseError(line)
-              #TODO: maybe try and parse these more intelligently
-              changes.append(line)
+    def add_change(self, change):
+        if self._changes is None:
+            self._changes = [change]
         else:
-          assert(False), "Unknown state: "+state
-
+            #Bit of trickery to keep the formatting nicer with a blank
+            #line at the end if there is one
+            changes = self._changes
+            changes.reverse()
+            added = False
+            for i in range(len(changes)):
+                m = blankline.match(changes[i])
+                if m is None:
+                    changes.insert(i, change)
+                    added = True
+                    break
+            changes.reverse()
+            if not added:
+                changes.append(change)
+            self._changes = changes
+
+    def __str__(self):
+        block = ""
+        if self.package is None:
+            raise ChangelogCreateError("Package not specified")
+        block += self.package + " "
+        if self._raw_version is None:
+            raise ChangelogCreateError("Version not specified")
+        block += "(" + self._raw_version + ") "
+        if self.distributions is None:
+            raise ChangelogCreateError("Distribution not specified")
+        block += self.distributions + "; "
+        if self.urgency is None:
+            raise ChangelogCreateError("Urgency not specified")
+        block += "urgency=" + self.urgency + self.urgency_comment
+        for (key, value) in self.other_pairs.items():
+            block += ", %s=%s" % (key, value)
+        block += '\n'
+        if self.changes() is None:
+            raise ChangelogCreateError("Changes not specified")
+        for change in self.changes():
+            block += change + "\n"
+        if not self._no_trailer:
+            if self.author is None:
+                raise ChangelogCreateError("Author not specified")
+            if self.date is None:
+                raise ChangelogCreateError("Date not specified")
+            block += " -- " + self.author + self._trailer_separator \
+                      + self.date + "\n"
+        for line in self._trailing:
+            block += line + "\n"
+        return block
+
+topline = re.compile('^(\w[-a-z0-9.+]+) \(([^\(\) \t]+)\)((\s+[-0-9a-z]+)+);',
+        re.IGNORECASE)
+blankline = re.compile('^\s*$')
+change = re.compile('^\s\s+.*$')
+endline = re.compile('^ -- (.*) <(.*)>(  ?)((\w+\,\s*)?\d{1,2}\s+\w+\s+'
+            '\d{4}\s+\d{1,2}:\d\d:\d\d\s+[-+]\d{4}(\s+\([^\\\(\)]\))?\s*)$')
+endline_nodetails = re.compile('^ --(?: (.*) <(.*)>(  ?)((\w+\,\s*)?\d{1,2}'
+                '\s+\w+\s+\d{4}\s+\d{1,2}:\d\d:\d\d\s+[-+]\d{4}'
+                '(\s+\([^\\\(\)]\))?))?\s*$')
+keyvalue= re.compile('^([-0-9a-z]+)=\s*(.*\S)$', re.IGNORECASE)
+value_re = re.compile('^([-0-9a-z]+)((\s+.*)?)$', re.IGNORECASE)
+xbcs_re = re.compile('^X[BCS]+-', re.IGNORECASE)
+emacs_variables = re.compile('^(;;\s*)?Local variables:', re.IGNORECASE)
+vim_variables = re.compile('^vim:', re.IGNORECASE)
+cvs_keyword = re.compile('^\$\w+:.*\$')
+comments = re.compile('^\# ')
+more_comments = re.compile('^/\*.*\*/')
+
+old_format_re1 = re.compile('^(\w+\s+\w+\s+\d{1,2} \d{1,2}:\d{1,2}:\d{1,2}'
+        '\s+[\w\s]*\d{4})\s+(.*)\s+(<|\()(.*)(\)|>)')
+old_format_re2 = re.compile('^(\w+\s+\w+\s+\d{1,2},?\s*\d{4})\s+(.*)'
+        '\s+(<|\()(.*)(\)|>)')
+old_format_re3 = re.compile('^(\w[-+0-9a-z.]*) \(([^\(\) \t]+)\)\;?',
+        re.IGNORECASE)
+old_format_re4 = re.compile('^([\w.+-]+)(-| )(\S+) Debian (\S+)',
+        re.IGNORECASE)
+old_format_re5 = re.compile('^Changes from version (.*) to (.*):',
+        re.IGNORECASE)
+old_format_re6 = re.compile('^Changes for [\w.+-]+-[\w.+-]+:?\s*$',
+        re.IGNORECASE)
+old_format_re7 = re.compile('^Old Changelog:\s*$', re.IGNORECASE)
+old_format_re8 = re.compile('^(?:\d+:)?\w[\w.+~-]*:?\s*$')
 
-      if state == inblock:
-        raise ChangelogParseError("Unexpected EOF")
 
-      #TODO: shouldn't be required should it?
-      self._blocks[-1].del_trailing_newline()
-
-  def get_version(self):
-    """Return a Version object for the last version"""
-    return self._blocks[0].version
+class Changelog(object):
+    """Represents a debian/changelog file. You can ask it several things
+       about the file.
+    """
 
-  def set_version(self, version):
-    """Set the version of the last changelog block
 
-    version can be a full version string, or a Version object
-    """
-    self._blocks[0].version = Version(version)
-
-  version = property(get_version, set_version,
-                     doc="Version object for last changelog block""")
-
-  ### For convenience, let's expose some of the version properties
-  full_version = property(lambda self: self.version.full_version,
-                  lambda self, v: setattr(self.version, 'full_version', v))
-  epoch = property(lambda self: self.version.epoch,
-                  lambda self, v: setattr(self.version, 'epoch', v))
-  debian_version = property(lambda self: self.version.debian_version,
-                  lambda self, v: setattr(self.version, 'debian_version', v))
-  upstream_version = property(lambda self: self.version.upstream_version,
-                  lambda self, v: setattr(self.version, 'upstream_version', v))
-
-  def get_package(self):
-    """Returns the name of the package in the last version."""
-    return self._blocks[0].package
+    def __init__(self, file=None, max_blocks=None,
+            allow_empty_author=False, strict=True):
+        """Set up the Changelog for use. file is the contects of the
+           changelog.
+        """
+        self._blocks = []
+        self.initial_blank_lines = []
+        if file is not None:
+            try:
+                self.parse_changelog(file, max_blocks=max_blocks,
+                        allow_empty_author=allow_empty_author,
+                        strict=strict)
+            except ChangelogParseError:
+                pass
+
+    def _parse_error(self, message, strict):
+        if strict:
+            raise ChangelogParseError(message)
+        else:
+            warnings.warn(message)
+
+    def parse_changelog(self, file, max_blocks=None,
+            allow_empty_author=False, strict=True):
+        first_heading = "first heading"
+        next_heading_or_eof = "next heading of EOF"
+        start_of_change_data = "start of change data"
+        more_changes_or_trailer = "more change data or trailer"
+        slurp_to_end = "slurp to end"
+
+        self._blocks = []
+        self.initial_blank_lines = []
+
+        current_block = ChangeBlock()
+        changes = []
+        
+        state = first_heading
+        old_state = None
+        if isinstance(file, basestring):
+            if file[-1] != '\n':
+                file += '\n'
+            file = file.split('\n')[:-1]
+        for line in file:
+            if state == first_heading or state == next_heading_or_eof:
+                top_match = topline.match(line)
+                blank_match = blankline.match(line)
+                if top_match is not None:
+                    if (max_blocks is not None
+                            and len(self._blocks) >= max_blocks):
+                        return
+                    current_block.package = top_match.group(1)
+                    current_block._raw_version = top_match.group(2)
+                    current_block.distributions = top_match.group(3).lstrip()
+
+                    pairs = line.split(";", 1)[1]
+                    all_keys = {}
+                    other_pairs = {}
+                    for pair in pairs.split(','):
+                        pair = pair.strip()
+                        kv_match = keyvalue.match(pair)
+                        if kv_match is None:
+                            self._parse_error("Invalid key-value "
+                                        "pair after ';': %s" % pair, strict)
+                            continue
+                        key = kv_match.group(1)
+                        value = kv_match.group(2)
+                        if key.lower() in all_keys:
+                            self._parse_error("Repeated key-value: "
+                                    "%s" % key.lower(), strict)
+                        all_keys[key.lower()] = value
+                        if key.lower() == "urgency":
+                            val_match = value_re.match(value)
+                            if val_match is None:
+                                self._parse_error("Badly formatted "
+                                        "urgency value: %s" % value, strict)
+                            else:
+                                current_block.urgency = val_match.group(1)
+                                comment = val_match.group(2)
+                                if comment is not None:
+                                    current_block.urgency_comment = comment
+                        else:
+                            other_pairs[key] = value
+                    current_block.other_pairs = other_pairs
+                    state = start_of_change_data
+                elif blank_match is not None:
+                    if state == first_heading:
+                        self.initial_blank_lines.append(line)
+                    else:
+                        self._blocks[-1].add_trailing_line(line)
+                else:
+                    emacs_match = emacs_variables.match(line)
+                    vim_match = vim_variables.match(line)
+                    cvs_match = cvs_keyword.match(line)
+                    comments_match = comments.match(line)
+                    more_comments_match = more_comments.match(line)
+                    if ((emacs_match is not None or vim_match is not None)
+                            and state != first_heading):
+                        self._blocks[-1].add_trailing_line(line)
+                        old_state = state
+                        state = slurp_to_end
+                        continue
+                    if (cvs_match is not None or comments_match is not None
+                            or more_comments_match is not None):
+                        if state == first_heading:
+                            self.initial_blank_lines.append(line)
+                        else:
+                            self._blocks[-1].add_trailing_line(line)
+                        continue
+                    if ((old_format_re1.match(line) is not None
+                        or old_format_re2.match(line) is not None
+                        or old_format_re3.match(line) is not None
+                        or old_format_re4.match(line) is not None
+                        or old_format_re5.match(line) is not None
+                        or old_format_re6.match(line) is not None
+                        or old_format_re7.match(line) is not None
+                        or old_format_re8.match(line) is not None)
+                        and state != first_heading):
+                            self._blocks[-1].add_trailing_line(line)
+                            old_state = state
+                            state = slurp_to_end
+                            continue
+                    self._parse_error("Unexpected line while looking "
+                            "for %s: %s" % (state, line), strict)
+                    if state == first_heading:
+                        self.initial_blank_lines.append(line)
+                    else:
+                        self._blocks[-1].add_trailing_line(line)
+            elif (state == start_of_change_data
+                    or state == more_changes_or_trailer):
+                change_match = change.match(line)
+                end_match = endline.match(line)
+                end_no_details_match = endline_nodetails.match(line)
+                blank_match = blankline.match(line)
+                if change_match is not None:
+                    changes.append(line)
+                    state = more_changes_or_trailer
+                elif end_match is not None:
+                    if end_match.group(3) != '  ':
+                        self._parse_error("Badly formatted trailer "
+                                "line: %s" % line, strict)
+                        current_block._trailer_separator = end_match.group(3)
+                    current_block.author = "%s <%s>" \
+                        % (end_match.group(1), end_match.group(2))
+                    current_block.date = end_match.group(4)
+                    current_block._changes = changes
+                    self._blocks.append(current_block)
+                    changes = []
+                    current_block = ChangeBlock()
+                    state = next_heading_or_eof
+                elif end_no_details_match is not None:
+                    if not allow_empty_author:
+                        self._parse_error("Badly formatted trailer "
+                                "line: %s" % line, strict)
+                        continue
+                    current_block._changes = changes
+                    self._blocks.append(current_block)
+                    changes = []
+                    current_block = ChangeBlock()
+                    state = next_heading_or_eof
+                elif blank_match is not None:
+                    changes.append(line)
+                else:
+                    cvs_match = cvs_keyword.match(line)
+                    comments_match = comments.match(line)
+                    more_comments_match = more_comments.match(line)
+                    if (cvs_match is not None or comments_match is not None
+                            or more_comments_match is not None):
+                        changes.append(line)
+                        continue
+                    self._parse_error("Unexpected line while looking "
+                            "for %s: %s" % (state, line), strict)
+                    changes.append(line)
+            elif state == slurp_to_end:
+                if old_state == next_heading_or_eof:
+                    self._blocks[-1].add_trailing_line(line)
+                else:
+                    changes.append(line)
+            else:
+                 assert False, "Unknown state: %s" % state
+                
+        if ((state != next_heading_or_eof and state != slurp_to_end)
+            or (state == slurp_to_end and old_state != next_heading_or_eof)):
+            self._parse_error("Found eof where expected %s" % state,
+                    strict)
+            current_block._changes = changes
+            current_block._no_trailer = True
+            self._blocks.append(current_block)
+
+    def get_version(self):
+        """Return a Version object for the last version"""
+        return self._blocks[0].version
+
+    def set_version(self, version):
+        """Set the version of the last changelog block
+
+        version can be a full version string, or a Version object
+        """
+        self._blocks[0].version = Version(version)
+
+    version = property(get_version, set_version,
+                 doc="Version object for last changelog block""")
+
+    ### For convenience, let's expose some of the version properties
+    full_version = property(lambda self: self.version.full_version)
+    epoch = property(lambda self: self.version.epoch)
+    debian_version = property(lambda self: self.version.debian_version)
+    upstream_version = property(lambda self: self.version.upstream_version)
+
+    def get_package(self):
+        """Returns the name of the package in the last version."""
+        return self._blocks[0].package
   
-  def set_package(self, package):
-    self._blocks[0].package = package
+    def set_package(self, package):
+        self._blocks[0].package = package
 
-  package = property(get_package, set_package,
+    package = property(get_package, set_package,
                      doc="Name of the package in the last version")
 
-  def get_versions(self):
-    """Returns a list of version objects that the package went through."""
-    return [block.version for block in self._blocks]
+    def get_versions(self):
+        """Returns a list of version objects that the package went through."""
+        return [block.version for block in self._blocks]
 
-  versions = property(get_versions,
+    versions = property(get_versions,
                       doc="List of version objects the package went through")
 
-  def __str__(self):
-    cl = ""
-    for block in self._blocks:
-      cl += str(block)
-    return cl
+    def _raw_versions(self):
+        return [block._raw_version for block in self._blocks]
+
+    def __str__(self):
+        cl = "\n".join(self.initial_blank_lines)
+        for block in self._blocks:
+            cl += str(block)
+        return cl
 
-  def set_distributions(self, distributions):
-    self._blocks[0].distributions = distributions
-  distributions = property(lambda self: self._blocks[0].distributions,
+    def set_distributions(self, distributions):
+        self._blocks[0].distributions = distributions
+    distributions = property(lambda self: self._blocks[0].distributions,
                            set_distributions)
 
-  def set_urgency(self, urgency):
-    self._blocks[0].urgency = urgency
-  urgency = property(lambda self: self._blocks[0].urgency, set_urgency)
+    def set_urgency(self, urgency):
+        self._blocks[0].urgency = urgency
+    urgency = property(lambda self: self._blocks[0].urgency, set_urgency)
 
-  def add_change(self, change):
-    self._blocks[0].add_change(change)
+    def add_change(self, change):
+        self._blocks[0].add_change(change)
 
-  def set_author(self, author):
-    self._blocks[0].author = author
-  author = property(lambda self: self._blocks[0].author, set_author)
+    def set_author(self, author):
+        self._blocks[0].author = author
+    author = property(lambda self: self._blocks[0].author, set_author)
 
-  def set_date(self, date):
-    self._blocks[0].date = date
-  date = property(lambda self: self._blocks[0].date, set_date)
+    def set_date(self, date):
+        self._blocks[0].date = date
+    date = property(lambda self: self._blocks[0].date, set_date)
 
-  def new_block(self, package=None, version=None, distributions=None,
-                urgency=None, changes=None, author=None, date=None):
-    block = ChangeBlock(package, version, distributions, urgency,
-                        changes, author, date)
-    block.add_trailing_newline()
-    self._blocks.insert(0, block)
+    def new_block(self, **kwargs):
+        block = ChangeBlock(**kwargs)
+        block.add_trailing_line('')
+        self._blocks.insert(0, block)
 
-  def write_to_open_file(self, file):
-    file.write(self.__str__())
+    def write_to_open_file(self, file):
+        file.write(self.__str__())
 
 def _test():
-  import doctest
-  doctest.testmod()
+    import doctest
+    doctest.testmod()
 
-  unittest.main()
+    unittest.main()
 
 class ChangelogTests(unittest.TestCase):
 
-  def test_create_changelog(self):
-
-    c = open('test_changelog').read()
-    cl = Changelog(c)
-    cs = str(cl)
-    clines = c.split('\n')
-    cslines = cs.split('\n')
-    for i in range(len(clines)):
-      self.assertEqual(clines[i], cslines[i])
-    self.assertEqual(len(clines), len(cslines), "Different lengths")
-
-  def test_create_changelog_single_block(self):
-
-    c = open('test_changelog').read()
-    cl = Changelog(c, max_blocks=1)
-    cs = str(cl)
-    self.assertEqual(cs,
-    """gnutls13 (1:1.4.1-1) unstable; urgency=HIGH
+    def test_create_changelog(self):
+        c = open('test_changelog').read()
+        cl = Changelog(c)
+        cs = str(cl)
+        clines = c.split('\n')
+        cslines = cs.split('\n')
+        for i in range(len(clines)):
+            self.assertEqual(clines[i], cslines[i])
+        self.assertEqual(len(clines), len(cslines), "Different lengths")
+
+    def test_create_changelog_single_block(self):
+        c = open('test_changelog').read()
+        cl = Changelog(c, max_blocks=1)
+        cs = str(cl)
+        self.assertEqual(cs,
+        """gnutls13 (1:1.4.1-1) unstable; urgency=HIGH
 
   [ James Westby ]
   * New upstream release.
@@ -385,159 +549,148 @@ class ChangelogTests(unittest.TestCase):
     gnutls-api so that devhelp can find it.
 
  -- Andreas Metzler <ametzler at debian.org>  Sat, 15 Jul 2006 11:11:08 +0200
+
 """)
 
-  def test_modify_changelog(self):
-
-    c = open('test_modify_changelog1').read()
-    cl = Changelog(c)
-    cl.package = 'gnutls14'
-    cl.version = '1:1.4.1-2'
-    cl.distributions = 'experimental'
-    cl.urgency = 'medium'
-    cl.add_change('  * Add magic foo')
-    cl.author = 'James Westby <jw+debian at jameswestby.net>'
-    cl.date = 'Sat, 16 Jul 2008 11:11:08 -0200'
-    c = open('test_modify_changelog2').read()
-    clines = c.split('\n')
-    cslines = str(cl).split('\n')
-    for i in range(len(clines)):
-      self.assertEqual(clines[i], cslines[i])
-    self.assertEqual(len(clines), len(cslines), "Different lengths")
-
-  def test_add_changelog_section(self):
-    c = open('test_modify_changelog2').read()
-    cl = Changelog(c)
-    cl.new_block('gnutls14', Version('1:1.4.1-3'), 'experimental', 'low',
-                    None, 'James Westby <jw+debian at jameswestby.net>')
-
-    self.assertRaises(ChangelogCreateError, cl.__str__)
-
-    cl.set_date('Sat, 16 Jul 2008 11:11:08 +0200')
-    cl.add_change('')
-    cl.add_change('  * Foo did not work, let us try bar')
-    cl.add_change('')
-
-    c = open('test_modify_changelog3').read()
-    clines = c.split('\n')
-    cslines = str(cl).split('\n')
-    for i in range(len(clines)):
-      self.assertEqual(clines[i], cslines[i])
-    self.assertEqual(len(clines), len(cslines), "Different lengths")
-
-  def test_strange_changelogs(self):
-    """ Just opens and parses a strange changelog """
-    c = open('test_strange_changelog').read()
-    cl = Changelog(c)
-
-  def test_set_version_with_string(self):
-    c1 = Changelog(open('test_modify_changelog1').read())
-    c2 = Changelog(open('test_modify_changelog1').read())
-
-    c1.version = '1:2.3.5-2'
-    c2.version = Version('1:2.3.5-2')
-    self.assertEqual(c1.version, c2.version)
-    self.assertEqual((c1.full_version, c1.epoch, c1.upstream_version,
-                      c1.debian_version),
-                     (c2.full_version, c2.epoch, c2.upstream_version,
-                      c2.debian_version))
-
-  def test_changelog_no_author(self):
-    cl_no_author = """gnutls13 (1:1.4.1-1) unstable; urgency=low
+    def test_modify_changelog(self):
+        c = open('test_modify_changelog1').read()
+        cl = Changelog(c)
+        cl.package = 'gnutls14'
+        cl.version = '1:1.4.1-2'
+        cl.distributions = 'experimental'
+        cl.urgency = 'medium'
+        cl.add_change('  * Add magic foo')
+        cl.author = 'James Westby <jw+debian at jameswestby.net>'
+        cl.date = 'Sat, 16 Jul 2008 11:11:08 -0200'
+        c = open('test_modify_changelog2').read()
+        clines = c.split('\n')
+        cslines = str(cl).split('\n')
+        for i in range(len(clines)):
+            self.assertEqual(clines[i], cslines[i])
+        self.assertEqual(len(clines), len(cslines), "Different lengths")
+
+    def test_add_changelog_section(self):
+        c = open('test_modify_changelog2').read()
+        cl = Changelog(c)
+        cl.new_block(package='gnutls14',
+                version=Version('1:1.4.1-3'),
+                distributions='experimental',
+                urgency='low',
+                author='James Westby <jw+debian at jameswestby.net>')
+
+        self.assertRaises(ChangelogCreateError, cl.__str__)
+
+        cl.set_date('Sat, 16 Jul 2008 11:11:08 +0200')
+        cl.add_change('')
+        cl.add_change('  * Foo did not work, let us try bar')
+        cl.add_change('')
+
+        c = open('test_modify_changelog3').read()
+        clines = c.split('\n')
+        cslines = str(cl).split('\n')
+        for i in range(len(clines)):
+            self.assertEqual(clines[i], cslines[i])
+        self.assertEqual(len(clines), len(cslines), "Different lengths")
+
+    def test_strange_changelogs(self):
+        """ Just opens and parses a strange changelog """
+        c = open('test_strange_changelog').read()
+        cl = Changelog(c)
+
+    def test_set_version_with_string(self):
+        c1 = Changelog(open('test_modify_changelog1').read())
+        c2 = Changelog(open('test_modify_changelog1').read())
+
+        c1.version = '1:2.3.5-2'
+        c2.version = Version('1:2.3.5-2')
+        self.assertEqual(c1.version, c2.version)
+        self.assertEqual((c1.full_version, c1.epoch, c1.upstream_version,
+                          c1.debian_version),
+                         (c2.full_version, c2.epoch, c2.upstream_version,
+                          c2.debian_version))
+
+    def test_changelog_no_author(self):
+        cl_no_author = """gnutls13 (1:1.4.1-1) unstable; urgency=low
 
   * New upstream release.
 
  --
 """
-    c1 = Changelog()
-    c1.parse_changelog(cl_no_author, allow_empty_author=True)
-    self.assertEqual(c1.author, None)
-    self.assertEqual(c1.date, None)
-    self.assertEqual(c1.package, "gnutls13")
-    c2 = Changelog()
-    self.assertRaises(ChangelogParseError, c2.parse_changelog, cl_no_author)
-
-  def test_magic_version_properties(self):
-    c = Changelog(open('test_changelog').read())
-
-    c.debian_version = '2'
-    self.assertEqual(c.debian_version, '2')
-    self.assertEqual(c.full_version, '1:1.4.1-2')
-
-    c.upstream_version = '1.4.2'
-    self.assertEqual(c.upstream_version, '1.4.2')
-    self.assertEqual(c.full_version, '1:1.4.2-2')
-
-    c.epoch = '2'
-    self.assertEqual(c.epoch, '2')
-    self.assertEqual(c.full_version, '2:1.4.2-2')
-
-    self.assertEqual(str(c.version), c.full_version)
-
-    c.full_version = '1:1.4.1-1'
-    self.assertEqual(c.full_version, '1:1.4.1-1')
-    self.assertEqual(c.epoch, '1')
-    self.assertEqual(c.upstream_version, '1.4.1')
-    self.assertEqual(c.debian_version, '1')
+        c1 = Changelog()
+        c1.parse_changelog(cl_no_author, allow_empty_author=True)
+        self.assertEqual(c1.author, None)
+        self.assertEqual(c1.date, None)
+        self.assertEqual(c1.package, "gnutls13")
+        c2 = Changelog()
+        self.assertRaises(ChangelogParseError, c2.parse_changelog, cl_no_author)
+
+    def test_magic_version_properties(self):
+        c = Changelog(open('test_changelog'))
+        self.assertEqual(c.debian_version, '1')
+        self.assertEqual(c.full_version, '1:1.4.1-1')
+        self.assertEqual(c.upstream_version, '1.4.1')
+        self.assertEqual(c.epoch, '1')
+        self.assertEqual(str(c.version), c.full_version)
 
 class VersionTests(unittest.TestCase):
 
-  def _test_version(self, full_version, epoch, upstream, debian):
-    v = Version(full_version)
-    self.assertEqual(v.full_version, full_version, "Full version broken")
-    self.assertEqual(v.epoch, epoch, "Epoch broken")
-    self.assertEqual(v.upstream_version, upstream, "Upstram broken")
-    self.assertEqual(v.debian_version, debian, "Debian broken")
-
-  def testversions(self):
-    self._test_version('1:1.4.1-1', '1', '1.4.1', '1')
-    self._test_version('7.1.ds-1', None, '7.1.ds', '1')
-    self._test_version('10.11.1.3-2', None, '10.11.1.3', '2')
-    self._test_version('4.0.1.3.dfsg.1-2', None, '4.0.1.3.dfsg.1', '2')
-    self._test_version('0.4.23debian1', None, '0.4.23debian1', None)
-    self._test_version('1.2.10+cvs20060429-1', None, '1.2.10+cvs20060429', '1')
-    self._test_version('0.2.0-1+b1', None, '0.2.0', '1+b1')
-    self._test_version('4.3.90.1svn-r21976-1', None, '4.3.90.1svn-r21976', '1')
-    self._test_version('1.5+E-14', None, '1.5+E', '14')
-    self._test_version('20060611-0.0', None, '20060611', '0.0')
-    self._test_version('0.52.2-5.1', None, '0.52.2', '5.1')
-    self._test_version('7.0-035+1', None, '7.0', '035+1')
-    self._test_version('1.1.0+cvs20060620-1+2.6.15-8', None,
-        '1.1.0+cvs20060620-1+2.6.15', '8')
-    self._test_version('1.1.0+cvs20060620-1+1.0', None, '1.1.0+cvs20060620',
-                       '1+1.0')
-    self._test_version('4.2.0a+stable-2sarge1', None, '4.2.0a+stable',
-                       '2sarge1')
-    self._test_version('1.8RC4b', None, '1.8RC4b', None)
-    self._test_version('0.9~rc1-1', None, '0.9~rc1', '1')
-    self._test_version('2:1.0.4+svn26-1ubuntu1', '2', '1.0.4+svn26',
-                       '1ubuntu1')
-    self._test_version('2:1.0.4~rc2-1', '2', '1.0.4~rc2', '1')
-
-  def test_version_updating(self):
-    v = Version('1:1.4.1-1')
-
-    v.debian_version = '2'
-    self.assertEqual(v.debian_version, '2')
-    self.assertEqual(v.full_version, '1:1.4.1-2')
-
-    v.upstream_version = '1.4.2'
-    self.assertEqual(v.upstream_version, '1.4.2')
-    self.assertEqual(v.full_version, '1:1.4.2-2')
-
-    v.epoch = '2'
-    self.assertEqual(v.epoch, '2')
-    self.assertEqual(v.full_version, '2:1.4.2-2')
-
-    self.assertEqual(str(v), v.full_version)
-
-    v.full_version = '1:1.4.1-1'
-    self.assertEqual(v.full_version, '1:1.4.1-1')
-    self.assertEqual(v.epoch, '1')
-    self.assertEqual(v.upstream_version, '1.4.1')
-    self.assertEqual(v.debian_version, '1')
+    def _test_version(self, full_version, epoch, upstream, debian):
+        v = Version(full_version)
+        self.assertEqual(v.full_version, full_version, "Full version broken")
+        self.assertEqual(v.epoch, epoch, "Epoch broken")
+        self.assertEqual(v.upstream_version, upstream, "Upstram broken")
+        self.assertEqual(v.debian_version, debian, "Debian broken")
+
+    def testversions(self):
+        self._test_version('1:1.4.1-1', '1', '1.4.1', '1')
+        self._test_version('7.1.ds-1', None, '7.1.ds', '1')
+        self._test_version('10.11.1.3-2', None, '10.11.1.3', '2')
+        self._test_version('4.0.1.3.dfsg.1-2', None, '4.0.1.3.dfsg.1', '2')
+        self._test_version('0.4.23debian1', None, '0.4.23debian1', None)
+        self._test_version('1.2.10+cvs20060429-1', None,
+                '1.2.10+cvs20060429', '1')
+        self._test_version('0.2.0-1+b1', None, '0.2.0', '1+b1')
+        self._test_version('4.3.90.1svn-r21976-1', None,
+                '4.3.90.1svn-r21976', '1')
+        self._test_version('1.5+E-14', None, '1.5+E', '14')
+        self._test_version('20060611-0.0', None, '20060611', '0.0')
+        self._test_version('0.52.2-5.1', None, '0.52.2', '5.1')
+        self._test_version('7.0-035+1', None, '7.0', '035+1')
+        self._test_version('1.1.0+cvs20060620-1+2.6.15-8', None,
+            '1.1.0+cvs20060620-1+2.6.15', '8')
+        self._test_version('1.1.0+cvs20060620-1+1.0', None,
+                '1.1.0+cvs20060620', '1+1.0')
+        self._test_version('4.2.0a+stable-2sarge1', None, '4.2.0a+stable',
+                           '2sarge1')
+        self._test_version('1.8RC4b', None, '1.8RC4b', None)
+        self._test_version('0.9~rc1-1', None, '0.9~rc1', '1')
+        self._test_version('2:1.0.4+svn26-1ubuntu1', '2', '1.0.4+svn26',
+                           '1ubuntu1')
+        self._test_version('2:1.0.4~rc2-1', '2', '1.0.4~rc2', '1')
+
+    def test_version_updating(self):
+        v = Version('1:1.4.1-1')
+
+        v.debian_version = '2'
+        self.assertEqual(v.debian_version, '2')
+        self.assertEqual(v.full_version, '1:1.4.1-2')
+
+        v.upstream_version = '1.4.2'
+        self.assertEqual(v.upstream_version, '1.4.2')
+        self.assertEqual(v.full_version, '1:1.4.2-2')
+
+        v.epoch = '2'
+        self.assertEqual(v.epoch, '2')
+        self.assertEqual(v.full_version, '2:1.4.2-2')
+
+        self.assertEqual(str(v), v.full_version)
+
+        v.full_version = '1:1.4.1-1'
+        self.assertEqual(v.full_version, '1:1.4.1-1')
+        self.assertEqual(v.epoch, '1')
+        self.assertEqual(v.upstream_version, '1.4.1')
+        self.assertEqual(v.debian_version, '1')
 
 if __name__ == "__main__":
-  _test()
+    _test()
 
-# vim:softtabstop=2 shiftwidth=2 expandtab
-- 
1.5.5.GIT




More information about the pkg-python-debian-commits mailing list