[python-debian/master 2/2] Deprecate the 'debian_bundle' package in favor of a new 'debian' package

John Wright jsw at debian.org
Sun Mar 14 06:39:35 UTC 2010


Now, we have a top-level lib/ directory, containing the debian package
and a debian_bundle package for backwards-compatibility.

Closes: #570210
---
 deb822.py                       |    6 -
 debian/changelog                |    2 +
 debian/rules                    |    4 +-
 debian_bundle/__init__.py       |    3 -
 debian_bundle/arfile.py         |  314 -----------
 debian_bundle/changelog.py      |  472 -----------------
 debian_bundle/deb822.py         | 1106 ---------------------------------------
 debian_bundle/debfile.py        |  282 ----------
 debian_bundle/debian_support.py |  652 -----------------------
 debian_bundle/debtags.py        |  505 ------------------
 debian_bundle/deprecation.py    |   38 --
 debian_bundle/doc-debtags       |   98 ----
 lib/deb822.py                   |    6 +
 lib/debian/__init__.py          |    3 +
 lib/debian/arfile.py            |  314 +++++++++++
 lib/debian/changelog.py         |  472 +++++++++++++++++
 lib/debian/deb822.py            | 1106 +++++++++++++++++++++++++++++++++++++++
 lib/debian/debfile.py           |  282 ++++++++++
 lib/debian/debian_support.py    |  652 +++++++++++++++++++++++
 lib/debian/debtags.py           |  505 ++++++++++++++++++
 lib/debian/deprecation.py       |   38 ++
 lib/debian/doc-debtags          |   98 ++++
 lib/debian_bundle/__init__.py   |    9 +
 setup.py.in                     |    3 +-
 tests/test_changelog.py         |    2 +-
 tests/test_deb822.py            |    2 +-
 tests/test_debfile.py           |    2 +-
 tests/test_debtags.py           |    2 +-
 28 files changed, 3495 insertions(+), 3483 deletions(-)
 delete mode 100644 deb822.py
 delete mode 100644 debian_bundle/__init__.py
 delete mode 100644 debian_bundle/arfile.py
 delete mode 100644 debian_bundle/changelog.py
 delete mode 100644 debian_bundle/deb822.py
 delete mode 100644 debian_bundle/debfile.py
 delete mode 100644 debian_bundle/debian_support.py
 delete mode 100644 debian_bundle/debtags.py
 delete mode 100644 debian_bundle/deprecation.py
 delete mode 100755 debian_bundle/doc-debtags
 create mode 100644 lib/deb822.py
 create mode 100644 lib/debian/__init__.py
 create mode 100644 lib/debian/arfile.py
 create mode 100644 lib/debian/changelog.py
 create mode 100644 lib/debian/deb822.py
 create mode 100644 lib/debian/debfile.py
 create mode 100644 lib/debian/debian_support.py
 create mode 100644 lib/debian/debtags.py
 create mode 100644 lib/debian/deprecation.py
 create mode 100755 lib/debian/doc-debtags
 create mode 100644 lib/debian_bundle/__init__.py

diff --git a/deb822.py b/deb822.py
deleted file mode 100644
index cd65685..0000000
--- a/deb822.py
+++ /dev/null
@@ -1,6 +0,0 @@
-import sys
-print >> sys.stderr, "WARNING:",    \
-        "the 'deb822' top-level module is *DEPRECATED*,",   \
-        "please use 'debian_bundle.deb822'"
-
-from debian_bundle.deb822 import *
diff --git a/debian/changelog b/debian/changelog
index 4dd43ec..c2b99d8 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -39,6 +39,8 @@ python-debian (0.1.15) UNRELEASED; urgency=low
 
   [ John Wright ]
   * changelog: Use debian_support.Version directly
+  * Deprecate the 'debian_bundle' package in favor of a new 'debian'
+    package (Closes: #570210)
 
  -- John Wright <jsw at debian.org>  Sat, 13 Mar 2010 21:53:18 -0700
 
diff --git a/debian/rules b/debian/rules
index 0c3d7bc..dc4360d 100755
--- a/debian/rules
+++ b/debian/rules
@@ -22,8 +22,8 @@ build-stamp: setup.py
 	cd tests && ./test_debtags.py
 	cd tests && ./test_changelog.py
 
-	python debian_bundle/debian_support.py
-	debian_bundle/doc-debtags > README.debtags
+	python lib/debian/debian_support.py
+	lib/debian/doc-debtags > README.debtags
 
 	touch $@
 
diff --git a/debian_bundle/__init__.py b/debian_bundle/__init__.py
deleted file mode 100644
index b28b04f..0000000
--- a/debian_bundle/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/debian_bundle/arfile.py b/debian_bundle/arfile.py
deleted file mode 100644
index 9ad757e..0000000
--- a/debian_bundle/arfile.py
+++ /dev/null
@@ -1,314 +0,0 @@
-# ArFile: a Python representation of ar (as in "man 1 ar") archives.
-# Copyright (C) 2007    Stefano Zacchiroli  <zack at debian.org>
-# Copyright (C) 2007    Filippo Giunchedi   <filippo at debian.org>
-#
-# 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
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program 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
-# General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-GLOBAL_HEADER = "!<arch>\n"
-GLOBAL_HEADER_LENGTH = len(GLOBAL_HEADER)
-
-FILE_HEADER_LENGTH = 60
-FILE_MAGIC = "`\n"
-
-class ArError(Exception):
-    pass
-
-class ArFile(object):
-    """ Representation of an ar archive, see man 1 ar.
-    
-    The interface of this class tries to mimick that of the TarFile module in
-    the standard library.
-    
-    ArFile objects have the following (read-only) properties:
-        - members       same as getmembers()
-    """
-
-    def __init__(self, filename=None, mode='r', fileobj=None):
-        """ Build an ar file representation starting from either a filename or
-        an existing file object. The only supported mode is 'r' """
-
-        self.__members = [] 
-        self.__members_dict = {}
-        self.__fname = filename
-        self.__fileobj = fileobj
-        
-        if mode == "r":
-            self.__index_archive()
-        pass    # TODO write support
-
-    def __index_archive(self):
-        if self.__fname:
-            fp = open(self.__fname, "rb")
-        elif self.__fileobj:
-            fp = self.__fileobj
-        else:
-            raise ArError, "Unable to open valid file"
-
-        if fp.read(GLOBAL_HEADER_LENGTH) != GLOBAL_HEADER:
-            raise ArError, "Unable to find global header"
-
-        while True:
-            newmember = ArMember.from_file(fp, self.__fname)
-            if not newmember:
-                break
-            self.__members.append(newmember)
-            self.__members_dict[newmember.name] = newmember
-            if newmember.size % 2 == 0: # even, no padding
-                fp.seek(newmember.size, 1) # skip to next header
-            else:
-                fp.seek(newmember.size + 1 , 1) # skip to next header
-        
-        if self.__fname:
-            fp.close()
-
-    def getmember(self, name):
-        """ Return the (last occurrence of a) member in the archive whose name
-        is 'name'. Raise KeyError if no member matches the given name.
-
-        Note that in case of name collisions the only way to retrieve all
-        members matching a given name is to use getmembers. """
-
-        return self.__members_dict[name]
-
-    def getmembers(self):
-        """ Return a list of all members contained in the archive.
-
-        The list has the same order of members in the archive and can contain
-        duplicate members (i.e. members with the same name) if they are
-        duplicate in the archive itself. """
-
-        return self.__members
-
-    members = property(getmembers)
-
-    def getnames(self):
-        """ Return a list of all member names in the archive. """
-
-        return map(lambda f: f.name, self.__members)
-
-    def extractall():
-        """ Not (yet) implemented. """
-
-        raise NotImpelementedError  # TODO
-
-    def extract(self, member, path):
-        """ Not (yet) implemented. """
-
-        raise NotImpelementedError  # TODO
-
-    def extractfile(self, member):
-        """ Return a file object corresponding to the requested member. A member
-        can be specified either as a string (its name) or as a ArMember
-        instance. """
-
-        for m in self.__members:
-            if isinstance(member, ArMember) and m.name == member.name:
-                return m
-            elif member == m.name:
-                return m
-            else:
-                return None 
-
-    # container emulation
-
-    def __iter__(self):
-        """ Iterate over the members of the present ar archive. """
-
-        return iter(self.__members)
-
-    def __getitem__(self, name):
-        """ Same as .getmember(name). """
-
-        return self.getmember(name)
-
-
-class ArMember(object):
-    """ Member of an ar archive.
-
-    Implements most of a file object interface: read, readline, next,
-    readlines, seek, tell, close.
-    
-    ArMember objects have the following (read-only) properties:
-        - name      member name in an ar archive
-        - mtime     modification time
-        - owner     owner user
-        - group     owner group
-        - fmode     file permissions
-        - size      size in bytes
-        - fname     file name"""
-
-    def __init__(self):
-        self.__name = None      # member name (i.e. filename) in the archive
-        self.__mtime = None     # last modification time
-        self.__owner = None     # owner user
-        self.__group = None     # owner group
-        self.__fmode = None     # permissions
-        self.__size = None      # member size in bytes
-        self.__fname = None     # file name associated with this member
-        self.__fp = None        # file pointer 
-        self.__offset = None    # start-of-data offset
-        self.__end = None       # end-of-data offset
-
-    def from_file(fp, fname):
-        """fp is an open File object positioned on a valid file header inside
-        an ar archive. Return a new ArMember on success, None otherwise. """
-
-        buf = fp.read(FILE_HEADER_LENGTH)
-
-        if not buf:
-            return None
-
-        # sanity checks
-        if len(buf) < FILE_HEADER_LENGTH:
-            raise IOError, "Incorrect header length"
-
-        if buf[58:60] != FILE_MAGIC:
-            raise IOError, "Incorrect file magic"
-
-        # http://en.wikipedia.org/wiki/Ar_(Unix)    
-        #from   to     Name                      Format
-        #0      15     File name                 ASCII
-        #16     27     File modification date    Decimal
-        #28     33     Owner ID                  Decimal
-        #34     39     Group ID                  Decimal
-        #40     47     File mode                 Octal
-        #48     57     File size in bytes        Decimal
-        #58     59     File magic                \140\012
-
-        # XXX struct.unpack can be used as well here
-        f = ArMember()
-        f.__name = buf[0:16].split("/")[0].strip()
-        f.__mtime = int(buf[16:28])
-        f.__owner = int(buf[28:34])
-        f.__group = int(buf[34:40])
-        f.__fmode  = buf[40:48]  # XXX octal value
-        f.__size  = int(buf[48:58])
-
-        f.__fname = fname
-        f.__offset = fp.tell() # start-of-data
-        f.__end = f.__offset + f.__size
-
-        return f
-
-    from_file = staticmethod(from_file)
-    
-    # file interface
-
-    # XXX this is not a sequence like file objects
-    def read(self, size=0):
-        if self.__fp is None:
-            self.__fp = open(self.__fname, "r")
-            self.__fp.seek(self.__offset)
-
-        cur = self.__fp.tell()
-
-        if size > 0 and size <= self.__end - cur: # there's room
-            return self.__fp.read(size)
-
-        if cur >= self.__end or cur < self.__offset:
-            return ''
-
-        return self.__fp.read(self.__end - cur)
-
-    def readline(self, size=None):
-        if self.__fp is None:
-            self.__fp = open(self.__fname, "r")
-            self.__fp.seek(self.__offset)
-
-        if size is not None: 
-            buf = self.__fp.readline(size)
-            if self.__fp.tell() > self.__end:
-                return ''
-
-            return buf
-
-        buf = self.__fp.readline()
-        if self.__fp.tell() > self.__end:
-            return ''
-        else:
-            return buf
-
-    def readlines(self, sizehint=0):
-        if self.__fp is None:
-            self.__fp = open(self.__fname, "r")
-            self.__fp.seek(self.__offset)
-        
-        buf = None
-        lines = []
-        while True: 
-            buf = self.readline()
-            if not buf: 
-                break
-            lines.append(buf)
-
-        return lines
-
-    def seek(self, offset, whence=0):
-        if self.__fp is None:
-            self.__fp = open(self.__fname, "r")
-            self.__fp.seek(self.__offset)
-
-        if self.__fp.tell() < self.__offset:
-            self.__fp.seek(self.__offset)
-
-        if whence < 2 and offset + self.__fp.tell() < self.__offset:
-            raise IOError, "Can't seek at %d" % offset
-        
-        if whence == 1:
-            self.__fp.seek(offset, 1)
-        elif whence == 0:
-            self.__fp.seek(self.__offset + offset, 0)
-        elif whence == 2:
-            self.__fp.seek(self.__end + offset, 0)
-
-    def tell(self):
-        if self.__fp is None:
-            self.__fp = open(self.__fname, "r")
-            self.__fp.seek(self.__offset)
-
-        cur = self.__fp.tell()
-        
-        if cur < self.__offset:
-            return 0L
-        else:
-            return cur - self.__offset
-
-    def close(self):
-        if self.__fp is not None:
-            self.__fp.close()
-   
-    def next(self):
-        return self.readline()
-    
-    def __iter__(self):
-        def nextline():
-            line = self.readline()
-            if line:
-                yield line
-
-        return iter(nextline())
-
-    name = property(lambda self: self.__name)
-    mtime = property(lambda self: self.__mtime)
-    owner = property(lambda self: self.__owner)
-    group = property(lambda self: self.__group)
-    fmode = property(lambda self: self.__fmode)
-    size = property(lambda self: self.__size)
-    fname = property(lambda self: self.__fname)
-
-if __name__ == '__main__':
-    # test
-    # ar r test.ar <file1> <file2> .. <fileN>
-    a = ArFile("test.ar")
-    print "\n".join(a.getnames())
diff --git a/debian_bundle/changelog.py b/debian_bundle/changelog.py
deleted file mode 100644
index 9ef7c49..0000000
--- a/debian_bundle/changelog.py
+++ /dev/null
@@ -1,472 +0,0 @@
-# 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
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program 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 General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# 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 re
-import warnings
-
-import debian_support
-
-class ChangelogParseError(StandardError):
-    """Indicates that the changelog could not be parsed"""
-    is_user_error = True
-
-    def __init__(self, line):
-        self._line=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"""
-
-class VersionError(StandardError):
-    """Indicates that the version does not conform to the required format"""
-
-    is_user_error = True
-
-    def __init__(self, version):
-        self._version=version
-
-    def __str__(self):
-        return "Could not parse version: "+self._version
-
-class Version(debian_support.Version):
-    """Represents a version of a Debian package."""
-    # debian_support.Version now has all the functionality we need
-
-class ChangeBlock(object):
-    """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
-
-    def changes(self):
-        return self._changes
-
-    def add_trailing_line(self, line):
-        self._trailing.append(line)
-
-    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._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(r'^(\w%(name_chars)s*) \(([^\(\) \t]+)\)'
-                     '((\s+%(name_chars)s+)+)\;'
-                     % {'name_chars': '[-+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*$')
-
-
-class Changelog(object):
-    """Represents a debian/changelog file. You can ask it several things
-       about the file.
-    """
-
-
-    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):
-            # Make sure the changelog file is not empty.
-            if file is None or len(file.strip()) == 0:
-                self._parse_error('Empty changelog file.', strict)
-                return
-
-            file = file.splitlines()
-        for line in file:
-            # Support both lists of lines without the trailing newline and
-            # those with trailing newlines (e.g. when given a file object
-            # directly)
-            line = line.rstrip('\n')
-            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_revision)
-    debian_revision = property(lambda self: self.version.debian_revision)
-    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
-
-    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]
-
-    versions = property(get_versions,
-                      doc="List of version objects the package went through")
-
-    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 __iter__(self):
-        return iter(self._blocks)
-
-    def __len__(self):
-        return len(self._blocks)
-
-    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 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_date(self, date):
-        self._blocks[0].date = date
-    date = property(lambda self: self._blocks[0].date, set_date)
-
-    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__())
diff --git a/debian_bundle/deb822.py b/debian_bundle/deb822.py
deleted file mode 100644
index 15eb056..0000000
--- a/debian_bundle/deb822.py
+++ /dev/null
@@ -1,1106 +0,0 @@
-# vim: fileencoding=utf-8
-#
-# A python interface for various rfc822-like formatted files used by Debian
-# (.changes, .dsc, Packages, Sources, etc)
-#
-# Copyright (C) 2005-2006  dann frazier <dannf at dannf.org>
-# Copyright (C) 2006-2008  John Wright <john at johnwright.org>
-# Copyright (C) 2006       Adeodato Simó <dato at net.com.org.es>
-# Copyright (C) 2008       Stefano Zacchiroli <zack at upsilon.cc>
-#
-# 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 the Free Software Foundation, either version 2
-# of the License, or (at your option) any later version.
-#
-# This program 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 General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-
-from deprecation import function_deprecated_by
-
-try:
-    import apt_pkg
-    _have_apt_pkg = True
-except ImportError:
-    _have_apt_pkg = False
-
-import new
-import re
-import string
-import sys
-import StringIO
-import UserDict
-
-class TagSectionWrapper(object, UserDict.DictMixin):
-    """Wrap a TagSection object, using its find_raw method to get field values
-
-    This allows us to pick which whitespace to strip off the beginning and end
-    of the data, so we don't lose leading newlines.
-    """
-
-    def __init__(self, section):
-        self.__section = section
-
-    def keys(self):
-        return self.__section.keys()
-
-    def __getitem__(self, key):
-        s = self.__section.find_raw(key)
-
-        if s is None:
-            raise KeyError(key)
-
-        # Get just the stuff after the first ':'
-        # Could use s.partition if we only supported python >= 2.5
-        data = s[s.find(':')+1:]
-
-        # Get rid of spaces and tabs after the ':', but not newlines, and strip
-        # off any newline at the end of the data.
-        return data.lstrip(' \t').rstrip('\n')
-
-class OrderedSet(object):
-    """A set-like object that preserves order when iterating over it
-
-    We use this to keep track of keys in Deb822Dict, because it's much faster
-    to look up if a key is in a set than in a list.
-    """
-
-    def __init__(self, iterable=[]):
-        self.__set = set()
-        self.__order = []
-        for item in iterable:
-            self.add(item)
-
-    def add(self, item):
-        if item not in self:
-            # set.add will raise TypeError if something's unhashable, so we
-            # don't have to handle that ourselves
-            self.__set.add(item)
-            self.__order.append(item)
-
-    def remove(self, item):
-        # set.remove will raise KeyError, so we don't need to handle that
-        # ourselves
-        self.__set.remove(item)
-        self.__order.remove(item)
-
-    def __iter__(self):
-        # Return an iterator of items in the order they were added
-        return iter(self.__order)
-
-    def __contains__(self, item):
-        # This is what makes OrderedSet faster than using a list to keep track
-        # of keys.  Lookup in a set is O(1) instead of O(n) for a list.
-        return item in self.__set
-
-    ### list-like methods
-    append = add
-
-    def extend(self, iterable):
-        for item in iterable:
-            self.add(item)
-    ###
-
-class Deb822Dict(object, UserDict.DictMixin):
-    # Subclassing UserDict.DictMixin because we're overriding so much dict
-    # functionality that subclassing dict requires overriding many more than
-    # the four methods that DictMixin requires.
-    """A dictionary-like object suitable for storing RFC822-like data.
-
-    Deb822Dict behaves like a normal dict, except:
-        - key lookup is case-insensitive
-        - key order is preserved
-        - if initialized with a _parsed parameter, it will pull values from
-          that dictionary-like object as needed (rather than making a copy).
-          The _parsed dict is expected to be able to handle case-insensitive
-          keys.
-
-    If _parsed is not None, an optional _fields parameter specifies which keys
-    in the _parsed dictionary are exposed.
-    """
-
-    # See the end of the file for the definition of _strI
-
-    def __init__(self, _dict=None, _parsed=None, _fields=None,
-                 encoding="utf-8"):
-        self.__dict = {}
-        self.__keys = OrderedSet()
-        self.__parsed = None
-        self.encoding = encoding
-
-        if _dict is not None:
-            # _dict may be a dict or a list of two-sized tuples
-            if hasattr(_dict, 'items'):
-                items = _dict.items()
-            else:
-                items = list(_dict)
-
-            try:
-                for k, v in items:
-                    self[k] = v
-            except ValueError:
-                this = len(self.__keys)
-                len_ = len(items[this])
-                raise ValueError('dictionary update sequence element #%d has '
-                    'length %d; 2 is required' % (this, len_))
-        
-        if _parsed is not None:
-            self.__parsed = _parsed
-            if _fields is None:
-                self.__keys.extend([ _strI(k) for k in self.__parsed.keys() ])
-            else:
-                self.__keys.extend([ _strI(f) for f in _fields if self.__parsed.has_key(f) ])
-        
-    ### BEGIN DictMixin methods
-
-    def __setitem__(self, key, value):
-        key = _strI(key)
-        self.__keys.add(key)
-        self.__dict[key] = value
-        
-    def __getitem__(self, key):
-        key = _strI(key)
-        try:
-            value = self.__dict[key]
-        except KeyError:
-            if self.__parsed is not None and key in self.__keys:
-                value = self.__parsed[key]
-            else:
-                raise
-
-        if isinstance(value, str):
-            # Always return unicode objects instead of strings
-            value = value.decode(self.encoding)
-        return value
-
-    def __delitem__(self, key):
-        key = _strI(key)
-        self.__keys.remove(key)
-        try:
-            del self.__dict[key]
-        except KeyError:
-            # If we got this far, the key was in self.__keys, so it must have
-            # only been in the self.__parsed dict.
-            pass
-
-    def has_key(self, key):
-        key = _strI(key)
-        return key in self.__keys
-    
-    def keys(self):
-        return [str(key) for key in self.__keys]
-    
-    ### END DictMixin methods
-
-    def __repr__(self):
-        return '{%s}' % ', '.join(['%r: %r' % (k, v) for k, v in self.items()])
-
-    def __eq__(self, other):
-        mykeys = self.keys(); mykeys.sort()
-        otherkeys = other.keys(); otherkeys.sort()
-        if not mykeys == otherkeys:
-            return False
-
-        for key in mykeys:
-            if self[key] != other[key]:
-                return False
-
-        # If we got here, everything matched
-        return True
-
-    def copy(self):
-        # Use self.__class__ so this works as expected for subclasses
-        copy = self.__class__(self)
-        return copy
-
-    # TODO implement __str__() and make dump() use that?
-
-
-class Deb822(Deb822Dict):
-
-    def __init__(self, sequence=None, fields=None, _parsed=None,
-                 encoding="utf-8"):
-        """Create a new Deb822 instance.
-
-        :param sequence: a string, or any any object that returns a line of
-            input each time, normally a file().  Alternately, sequence can
-            be a dict that contains the initial key-value pairs.
-
-        :param fields: if given, it is interpreted as a list of fields that
-            should be parsed (the rest will be discarded).
-
-        :param _parsed: internal parameter.
-
-        :param encoding: When parsing strings, interpret them in this encoding.
-            (All values are given back as unicode objects, so an encoding is
-            necessary in order to properly interpret the strings.)
-        """
-
-        if hasattr(sequence, 'items'):
-            _dict = sequence
-            sequence = None
-        else:
-            _dict = None
-        Deb822Dict.__init__(self, _dict=_dict, _parsed=_parsed, _fields=fields,
-                            encoding=encoding)
-
-        if sequence is not None:
-            try:
-                self._internal_parser(sequence, fields)
-            except EOFError:
-                pass
-
-        self.gpg_info = None
-
-    def iter_paragraphs(cls, sequence, fields=None, use_apt_pkg=True,
-                        shared_storage=False, encoding="utf-8"):
-        """Generator that yields a Deb822 object for each paragraph in sequence.
-
-        :param sequence: same as in __init__.
-
-        :param fields: likewise.
-
-        :param use_apt_pkg: if sequence is a file(), apt_pkg will be used 
-            if available to parse the file, since it's much much faster.  Set
-            this parameter to False to disable using apt_pkg.
-        :param shared_storage: not used, here for historical reasons.  Deb822
-            objects never use shared storage anymore.
-        :param encoding: Interpret the paragraphs in this encoding.
-            (All values are given back as unicode objects, so an encoding is
-            necessary in order to properly interpret the strings.)
-        """
-
-        if _have_apt_pkg and use_apt_pkg and isinstance(sequence, file):
-            parser = apt_pkg.TagFile(sequence)
-            for section in parser:
-                yield cls(fields=fields, _parsed=TagSectionWrapper(section),
-                          encoding=encoding)
-
-        else:
-            iterable = iter(sequence)
-            x = cls(iterable, fields, encoding=encoding)
-            while len(x) != 0:
-                yield x
-                x = cls(iterable, fields, encoding=encoding)
-
-    iter_paragraphs = classmethod(iter_paragraphs)
-
-    ###
-
-    def _internal_parser(self, sequence, fields=None):
-        single = re.compile("^(?P<key>\S+)\s*:\s*(?P<data>\S.*?)\s*$")
-        multi = re.compile("^(?P<key>\S+)\s*:\s*$")
-        multidata = re.compile("^\s(?P<data>.+?)\s*$")
-
-        wanted_field = lambda f: fields is None or f in fields
-
-        if isinstance(sequence, basestring):
-            sequence = sequence.splitlines()
-
-        curkey = None
-        content = ""
-        for line in self.gpg_stripped_paragraph(sequence):
-            if isinstance(line, str):
-                line = line.decode(self.encoding)
-            m = single.match(line)
-            if m:
-                if curkey:
-                    self[curkey] += content
-
-                if not wanted_field(m.group('key')):
-                    curkey = None
-                    continue
-
-                curkey = m.group('key')
-                self[curkey] = m.group('data')
-                content = ""
-                continue
-
-            m = multi.match(line)
-            if m:
-                if curkey:
-                    self[curkey] += content
-
-                if not wanted_field(m.group('key')):
-                    curkey = None
-                    continue
-
-                curkey = m.group('key')
-                self[curkey] = ""
-                content = ""
-                continue
-
-            m = multidata.match(line)
-            if m:
-                content += '\n' + line # XXX not m.group('data')?
-                continue
-
-        if curkey:
-            self[curkey] += content
-
-    def __str__(self):
-        return self.dump()
-
-    def __unicode__(self):
-        return self.dump()
-
-    # __repr__ is handled by Deb822Dict
-
-    def get_as_string(self, key):
-        """Return the self[key] as a string (or unicode)
-
-        The default implementation just returns unicode(self[key]); however,
-        this can be overridden in subclasses (e.g. _multivalued) that can take
-        special values.
-        """
-        return unicode(self[key])
-
-    def dump(self, fd=None, encoding=None):
-        """Dump the the contents in the original format
-
-        If fd is None, return a unicode object.
-
-        If fd is not None, attempt to encode the output to the encoding the
-        object was initialized with, or the value of the encoding argument if
-        it is not None.  This will raise UnicodeEncodeError if the encoding
-        can't support all the characters in the Deb822Dict values.
-        """
-
-        if fd is None:
-            fd = StringIO.StringIO()
-            return_string = True
-        else:
-            return_string = False
-
-        if encoding is None:
-            # Use the encoding we've been using to decode strings with if none
-            # was explicitly specified
-            encoding = self.encoding
-
-        for key in self.iterkeys():
-            value = self.get_as_string(key)
-            if not value or value[0] == '\n':
-                # Avoid trailing whitespace after "Field:" if it's on its own
-                # line or the value is empty
-                # XXX Uh, really print value if value == '\n'?
-                entry = '%s:%s\n' % (key, value)
-            else:
-                entry = '%s: %s\n' % (key, value)
-            if not return_string:
-                fd.write(entry.encode(encoding))
-            else:
-                fd.write(entry)
-        if return_string:
-            return fd.getvalue()
-
-    ###
-
-    def is_single_line(self, s):
-        if s.count("\n"):
-            return False
-        else:
-            return True
-
-    isSingleLine = function_deprecated_by(is_single_line)
-
-    def is_multi_line(self, s):
-        return not self.is_single_line(s)
-
-    isMultiLine = function_deprecated_by(is_multi_line)
-
-    def _merge_fields(self, s1, s2):
-        if not s2:
-            return s1
-        if not s1:
-            return s2
-
-        if self.is_single_line(s1) and self.is_single_line(s2):
-            ## some fields are delimited by a single space, others
-            ## a comma followed by a space.  this heuristic assumes
-            ## that there are multiple items in one of the string fields
-            ## so that we can pick up on the delimiter being used
-            delim = ' '
-            if (s1 + s2).count(', '):
-                delim = ', '
-
-            L = (s1 + delim + s2).split(delim)
-            L.sort()
-
-            prev = merged = L[0]
-
-            for item in L[1:]:
-                ## skip duplicate entries
-                if item == prev:
-                    continue
-                merged = merged + delim + item
-                prev = item
-            return merged
-
-        if self.is_multi_line(s1) and self.is_multi_line(s2):
-            for item in s2.splitlines(True):
-                if item not in s1.splitlines(True):
-                    s1 = s1 + "\n" + item
-            return s1
-
-        raise ValueError
-
-    _mergeFields = function_deprecated_by(_merge_fields)
-
-    def merge_fields(self, key, d1, d2=None):
-        ## this method can work in two ways - abstract that away
-        if d2 == None:
-            x1 = self
-            x2 = d1
-        else:
-            x1 = d1
-            x2 = d2
-
-        ## we only have to do work if both objects contain our key
-        ## otherwise, we just take the one that does, or raise an
-        ## exception if neither does
-        if key in x1 and key in x2:
-            merged = self._mergeFields(x1[key], x2[key])
-        elif key in x1:
-            merged = x1[key]
-        elif key in x2:
-            merged = x2[key]
-        else:
-            raise KeyError
-
-        ## back to the two different ways - if this method was called
-        ## upon an object, update that object in place.
-        ## return nothing in this case, to make the author notice a
-        ## problem if she assumes the object itself will not be modified
-        if d2 == None:
-            self[key] = merged
-            return None
-
-        return merged
-
-    mergeFields = function_deprecated_by(merge_fields)
-
-    def split_gpg_and_payload(sequence):
-        """Return a (gpg_pre, payload, gpg_post) tuple
-
-        Each element of the returned tuple is a list of lines (with trailing
-        whitespace stripped).
-        """
-
-        gpg_pre_lines = []
-        lines = []
-        gpg_post_lines = []
-        state = 'SAFE'
-        gpgre = re.compile(r'^-----(?P<action>BEGIN|END) PGP (?P<what>[^-]+)-----$')
-        blank_line = re.compile('^$')
-        first_line = True
-
-        for line in sequence:
-            line = line.strip('\r\n')
-
-            # skip initial blank lines, if any
-            if first_line:
-                if blank_line.match(line):
-                    continue
-                else:
-                    first_line = False
-
-            m = gpgre.match(line)
-
-            if not m:
-                if state == 'SAFE':
-                    if not blank_line.match(line):
-                        lines.append(line)
-                    else:
-                        if not gpg_pre_lines:
-                            # There's no gpg signature, so we should stop at
-                            # this blank line
-                            break
-                elif state == 'SIGNED MESSAGE':
-                    if blank_line.match(line):
-                        state = 'SAFE'
-                    else:
-                        gpg_pre_lines.append(line)
-                elif state == 'SIGNATURE':
-                    gpg_post_lines.append(line)
-            else:
-                if m.group('action') == 'BEGIN':
-                    state = m.group('what')
-                elif m.group('action') == 'END':
-                    gpg_post_lines.append(line)
-                    break
-                if not blank_line.match(line):
-                    if not lines:
-                        gpg_pre_lines.append(line)
-                    else:
-                        gpg_post_lines.append(line)
-
-        if len(lines):
-            return (gpg_pre_lines, lines, gpg_post_lines)
-        else:
-            raise EOFError('only blank lines found in input')
-
-    split_gpg_and_payload = staticmethod(split_gpg_and_payload)
-
-    def gpg_stripped_paragraph(cls, sequence):
-        return cls.split_gpg_and_payload(sequence)[1]
-
-    gpg_stripped_paragraph = classmethod(gpg_stripped_paragraph)
-
-    def get_gpg_info(self):
-        """Return a GpgInfo object with GPG signature information
-
-        This method will raise ValueError if the signature is not available
-        (e.g. the original text cannot be found)"""
-
-        # raw_text is saved (as a string) only for Changes and Dsc (see
-        # _gpg_multivalued.__init__) which is small compared to Packages or
-        # Sources which contain no signature
-        if not hasattr(self, 'raw_text'):
-            raise ValueError, "original text cannot be found"
-
-        if self.gpg_info is None:
-            self.gpg_info = GpgInfo.from_sequence(self.raw_text)
-
-        return self.gpg_info
-
-###
-
-# XXX check what happens if input contains more that one signature
-class GpgInfo(dict):
-    """A wrapper around gnupg parsable output obtained via --status-fd
-
-    This class is really a dictionary containing parsed output from gnupg plus
-    some methods to make sense of the data.
-    Keys are keywords and values are arguments suitably splitted.
-    See /usr/share/doc/gnupg/DETAILS.gz"""
-
-    # keys with format "key keyid uid"
-    uidkeys = ('GOODSIG', 'EXPSIG', 'EXPKEYSIG', 'REVKEYSIG', 'BADSIG')
-
-    def valid(self):
-        """Is the signature valid?"""
-        return self.has_key('GOODSIG') or self.has_key('VALIDSIG')
-    
-# XXX implement as a property?
-# XXX handle utf-8 %-encoding
-    def uid(self):
-        """Return the primary ID of the signee key, None is not available"""
-        pass
-
-    @staticmethod
-    def from_output(out, err=None):
-        """Create a new GpgInfo object from gpg(v) --status-fd output (out) and
-        optionally collect stderr as well (err).
-        
-        Both out and err can be lines in newline-terminated sequence or regular strings."""
-
-        n = GpgInfo()
-
-        if isinstance(out, basestring):
-            out = out.split('\n')
-        if isinstance(err, basestring):
-            err = err.split('\n')
-
-        n.out = out
-        n.err = err
-        
-        header = '[GNUPG:] '
-        for l in out:
-            if not l.startswith(header):
-                continue
-
-            l = l[len(header):]
-            l = l.strip('\n')
-
-            # str.partition() would be better, 2.5 only though
-            s = l.find(' ')
-            key = l[:s]
-            if key in GpgInfo.uidkeys:
-                # value is "keyid UID", don't split UID
-                value = l[s+1:].split(' ', 1)
-            else:
-                value = l[s+1:].split(' ')
-
-            n[key] = value
-        return n 
-
-# XXX how to handle sequences of lines? file() returns \n-terminated
-    @staticmethod
-    def from_sequence(sequence, keyrings=['/usr/share/keyrings/debian-keyring.gpg'],
-            executable=["/usr/bin/gpgv"]):
-        """Create a new GpgInfo object from the given sequence.
-
-        Sequence is a sequence of lines or a string
-        executable is a list of args for subprocess.Popen, the first element being the gpg executable"""
-
-        # XXX check for gpg as well and use --verify accordingly?
-        args = executable
-        #args.extend(["--status-fd", "1", "--no-default-keyring"])
-        args.extend(["--status-fd", "1"])
-        import os
-        [args.extend(["--keyring", k]) for k in keyrings if os.path.isfile(k) and os.access(k, os.R_OK)]
-        
-        if "--keyring" not in args:
-            raise IOError, "cannot access none of given keyrings"
-
-        import subprocess
-        p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-        # XXX what to do with exit code?
-
-        if isinstance(sequence, basestring):
-            (out, err) = p.communicate(sequence)
-        else:
-            (out, err) = p.communicate("\n".join(sequence))
-
-        return GpgInfo.from_output(out, err)
-
-    @staticmethod
-    def from_file(target, *args):
-        """Create a new GpgInfo object from the given file, calls from_sequence(file(target), *args)"""
-        return from_sequence(file(target), *args)
-    
-###
-
-class PkgRelation(object):
-    """Inter-package relationships
-
-    Structured representation of the relationships of a package to another,
-    i.e. of what can appear in a Deb882 field like Depends, Recommends,
-    Suggests, ... (see Debian Policy 7.1).
-    """
-
-    # XXX *NOT* a real dependency parser, and that is not even a goal here, we
-    # just parse as much as we need to split the various parts composing a
-    # dependency, checking their correctness wrt policy is out of scope
-    __dep_RE = re.compile( \
-            r'^\s*(?P<name>[a-zA-Z0-9.+\-]{2,})(\s*\(\s*(?P<relop>[>=<]+)\s*(?P<version>[0-9a-zA-Z:\-+~.]+)\s*\))?(\s*\[(?P<archs>[\s!\w\-]+)\])?\s*$')
-    __comma_sep_RE = re.compile(r'\s*,\s*')
-    __pipe_sep_RE = re.compile(r'\s*\|\s*')
-    __blank_sep_RE = re.compile(r'\s*')
-
-    @classmethod
-    def parse_relations(cls, raw):
-        """Parse a package relationship string (i.e. the value of a field like
-        Depends, Recommends, Build-Depends ...)
-        """
-        def parse_archs(raw):
-            # assumption: no space beween '!' and architecture name
-            archs = []
-            for arch in cls.__blank_sep_RE.split(raw.strip()):
-                if len(arch) and arch[0] == '!':
-                    archs.append((False, arch[1:]))
-                else:
-                    archs.append((True, arch))
-            return archs
-
-        def parse_rel(raw):
-            match = cls.__dep_RE.match(raw)
-            if match:
-                parts = match.groupdict()
-                d = { 'name': parts['name'] }
-                if not (parts['relop'] is None or parts['version'] is None):
-                    d['version'] = (parts['relop'], parts['version'])
-                else:
-                    d['version'] = None
-                if parts['archs'] is None:
-                    d['arch'] = None
-                else:
-                    d['arch'] = parse_archs(parts['archs'])
-                return d
-            else:
-                print >> sys.stderr, \
-                        'deb822.py: WARNING: cannot parse package' \
-                        ' relationship "%s", returning it raw' % raw
-                return { 'name': raw, 'version': None, 'arch': None }
-
-        tl_deps = cls.__comma_sep_RE.split(raw.strip()) # top-level deps
-        cnf = map(cls.__pipe_sep_RE.split, tl_deps)
-        return map(lambda or_deps: map(parse_rel, or_deps), cnf)
-
-    @staticmethod
-    def str(rels):
-        """Format to string structured inter-package relationships
-        
-        Perform the inverse operation of parse_relations, returning a string
-        suitable to be written in a package stanza.
-        """
-        def pp_arch(arch_spec):
-            (excl, arch) = arch_spec
-            if excl:
-                return arch
-            else:
-                return '!' + arch
-
-        def pp_atomic_dep(dep):
-            s = dep['name']
-            if dep.has_key('version') and dep['version'] is not None:
-                s += ' (%s %s)' % dep['version']
-            if dep.has_key('arch') and dep['arch'] is not None:
-                s += ' [%s]' % string.join(map(pp_arch, dep['arch']))
-            return s
-
-        pp_or_dep = lambda deps: string.join(map(pp_atomic_dep, deps), ' | ')
-        return string.join(map(pp_or_dep, rels), ', ')
-
-
-class _lowercase_dict(dict):
-    """Dictionary wrapper which lowercase keys upon lookup."""
-
-    def __getitem__(self, key):
-        return dict.__getitem__(self, key.lower())
-
-
-class _PkgRelationMixin(object):
-    """Package relationship mixin
-
-    Inheriting from this mixin you can extend a Deb882 object with attributes
-    letting you access inter-package relationship in a structured way, rather
-    than as strings. For example, while you can usually use pkg['depends'] to
-    obtain the Depends string of package pkg, mixing in with this class you
-    gain pkg.depends to access Depends as a Pkgrel instance
-
-    To use, subclass _PkgRelationMixin from a class with a _relationship_fields
-    attribute. It should be a list of field names for which structured access
-    is desired; for each of them a method wild be added to the inherited class.
-    The method name will be the lowercase version of field name; '-' will be
-    mangled as '_'. The method would return relationships in the same format of
-    the PkgRelation' relations property.
-
-    See Packages and Sources as examples.
-    """
-
-    def __init__(self, *args, **kwargs):
-        self.__relations = _lowercase_dict({})
-        self.__parsed_relations = False
-        for name in self._relationship_fields:
-            # To avoid reimplementing Deb822 key lookup logic we use a really
-            # simple dict subclass which just lowercase keys upon lookup. Since
-            # dictionary building happens only here, we ensure that all keys
-            # are in fact lowercase.
-            # With this trick we enable users to use the same key (i.e. field
-            # name) of Deb822 objects on the dictionary returned by the
-            # relations property.
-            keyname = name.lower()
-            if self.has_key(name):
-                self.__relations[keyname] = None   # lazy value
-                    # all lazy values will be expanded before setting
-                    # __parsed_relations to True
-            else:
-                self.__relations[keyname] = []
-
-    @property
-    def relations(self):
-        """Return a dictionary of inter-package relationships among the current
-        and other packages.
-
-        Dictionary keys depend on the package kind. Binary packages have keys
-        like 'depends', 'recommends', ... while source packages have keys like
-        'build-depends', 'build-depends-indep' and so on. See the Debian policy
-        for the comprehensive field list.
-
-        Dictionary values are package relationships returned as lists of lists
-        of dictionaries (see below for some examples).
-
-        The encoding of package relationships is as follows:
-        - the top-level lists corresponds to the comma-separated list of
-          Deb822, their components form a conjuction, i.e. they have to be
-          AND-ed together
-        - the inner lists corresponds to the pipe-separated list of Deb822,
-          their components form a disjunction, i.e. they have to be OR-ed
-          together
-        - member of the inner lists are dictionaries with the following keys:
-          - name:       package (or virtual package) name
-          - version:    A pair <operator, version> if the relationship is
-                        versioned, None otherwise. operator is one of "<<",
-                        "<=", "=", ">=", ">>"; version is the given version as
-                        a string.
-          - arch:       A list of pairs <polarity, architecture> if the
-                        relationship is architecture specific, None otherwise.
-                        Polarity is a boolean (false if the architecture is
-                        negated with "!", true otherwise), architecture the
-                        Debian archtiecture name as a string.
-
-        Examples:
-
-          "emacs | emacsen, make, debianutils (>= 1.7)"     becomes
-          [ [ {'name': 'emacs'}, {'name': 'emacsen'} ],
-            [ {'name': 'make'} ],
-            [ {'name': 'debianutils', 'version': ('>=', '1.7')} ] ]
-
-          "tcl8.4-dev, procps [!hurd-i386]"                 becomes
-          [ [ {'name': 'tcl8.4-dev'} ],
-            [ {'name': 'procps', 'arch': (false, 'hurd-i386')} ] ]
-        """
-        if not self.__parsed_relations:
-            lazy_rels = filter(lambda n: self.__relations[n] is None,
-                    self.__relations.keys())
-            for n in lazy_rels:
-                self.__relations[n] = PkgRelation.parse_relations(self[n])
-            self.__parsed_relations = True
-        return self.__relations
-
-class _multivalued(Deb822):
-    """A class with (R/W) support for multivalued fields.
-
-    To use, create a subclass with a _multivalued_fields attribute.  It should
-    be a dictionary with *lower-case* keys, with lists of human-readable
-    identifiers of the fields as the values.  Please see Dsc, Changes, and
-    PdiffIndex as examples.
-    """
-
-    def __init__(self, *args, **kwargs):
-        Deb822.__init__(self, *args, **kwargs)
-
-        for field, fields in self._multivalued_fields.items():
-            try:
-                contents = self[field]
-            except KeyError:
-                continue
-
-            if self.is_multi_line(contents):
-                self[field] = []
-                updater_method = self[field].append
-            else:
-                self[field] = Deb822Dict()
-                updater_method = self[field].update
-
-            for line in filter(None, contents.splitlines()):
-                updater_method(Deb822Dict(zip(fields, line.split())))
-
-    def get_as_string(self, key):
-        keyl = key.lower()
-        if keyl in self._multivalued_fields:
-            fd = StringIO.StringIO()
-            if hasattr(self[key], 'keys'): # single-line
-                array = [ self[key] ]
-            else: # multi-line
-                fd.write("\n")
-                array = self[key]
-
-            order = self._multivalued_fields[keyl]
-            try:
-                field_lengths = self._fixed_field_lengths
-            except AttributeError:
-                field_lengths = {}
-            for item in array:
-                for x in order:
-                    raw_value = str(item[x])
-                    try:
-                        length = field_lengths[keyl][x]
-                    except KeyError:
-                        value = raw_value
-                    else:
-                        value = (length - len(raw_value)) * " " + raw_value
-                    fd.write(" %s" % value)
-                fd.write("\n")
-            return fd.getvalue().rstrip("\n")
-        else:
-            return Deb822.get_as_string(self, key)
-
-###
-
-
-class _gpg_multivalued(_multivalued):
-    """A _multivalued class that can support gpg signed objects
-
-    This class's feature is that it stores the raw text before parsing so that
-    gpg can verify the signature.  Use it just like you would use the
-    _multivalued class.
-
-    This class only stores raw text if it is given a raw string, or if it
-    detects a gpg signature when given a file or sequence of lines (see
-    Deb822.split_gpg_and_payload for details).
-    """
-
-    def __init__(self, *args, **kwargs):
-        try:
-            sequence = args[0]
-        except IndexError:
-            sequence = kwargs.get("sequence", None)
-
-        if sequence is not None:
-            if isinstance(sequence, basestring):
-                self.raw_text = sequence
-            elif hasattr(sequence, "items"):
-                # sequence is actually a dict(-like) object, so we don't have
-                # the raw text.
-                pass
-            else:
-                try:
-                    gpg_pre_lines, lines, gpg_post_lines = \
-                            self.split_gpg_and_payload(sequence)
-                except EOFError:
-                    # Empty input
-                    gpg_pre_lines = lines = gpg_post_lines = []
-                if gpg_pre_lines and gpg_post_lines:
-                    raw_text = StringIO.StringIO()
-                    raw_text.write("\n".join(gpg_pre_lines))
-                    raw_text.write("\n\n")
-                    raw_text.write("\n".join(lines))
-                    raw_text.write("\n\n")
-                    raw_text.write("\n".join(gpg_post_lines))
-                    self.raw_text = raw_text.getvalue()
-                try:
-                    args = list(args)
-                    args[0] = lines
-                except IndexError:
-                    kwargs["sequence"] = lines
-
-        _multivalued.__init__(self, *args, **kwargs)
-
-
-class Dsc(_gpg_multivalued):
-    _multivalued_fields = {
-        "files": [ "md5sum", "size", "name" ],
-        "checksums-sha1": ["sha1", "size", "name"],
-        "checksums-sha256": ["sha256", "size", "name"],
-    }
-
-
-class Changes(_gpg_multivalued):
-    _multivalued_fields = {
-        "files": [ "md5sum", "size", "section", "priority", "name" ],
-        "checksums-sha1": ["sha1", "size", "name"],
-        "checksums-sha256": ["sha256", "size", "name"],
-    }
-
-    def get_pool_path(self):
-        """Return the path in the pool where the files would be installed"""
-    
-        # This is based on the section listed for the first file.  While
-        # it is possible, I think, for a package to provide files in multiple
-        # sections, I haven't seen it in practice.  In any case, this should
-        # probably detect such a situation and complain, or return a list...
-        
-        s = self['files'][0]['section']
-
-        try:
-            section, subsection = s.split('/')
-        except ValueError:
-            # main is implicit
-            section = 'main'
-
-        if self['source'].startswith('lib'):
-            subdir = self['source'][:4]
-        else:
-            subdir = self['source'][0]
-
-        return 'pool/%s/%s/%s' % (section, subdir, self['source'])
-
-
-class PdiffIndex(_multivalued):
-    _multivalued_fields = {
-        "sha1-current": [ "SHA1", "size" ],
-        "sha1-history": [ "SHA1", "size", "date" ],
-        "sha1-patches": [ "SHA1", "size", "date" ],
-    }
-
-    @property
-    def _fixed_field_lengths(self):
-        fixed_field_lengths = {}
-        for key in self._multivalued_fields:
-            if hasattr(self[key], 'keys'):
-                # Not multi-line -- don't need to compute the field length for
-                # this one
-                continue
-            length = self._get_size_field_length(key)
-            fixed_field_lengths[key] = {"size": length}
-        return fixed_field_lengths
-
-    def _get_size_field_length(self, key):
-        lengths = [len(str(item['size'])) for item in self[key]]
-        return max(lengths)
-
-
-class Release(_multivalued):
-    """Represents a Release file
-
-    Set the size_field_behavior attribute to "dak" to make the size field
-    length only as long as the longest actual value.  The default,
-    "apt-ftparchive" makes the field 16 characters long regardless.
-    """
-    # FIXME: Add support for detecting the behavior of the input, if
-    # constructed from actual 822 text.
-
-    _multivalued_fields = {
-        "md5sum": [ "md5sum", "size", "name" ],
-        "sha1": [ "sha1", "size", "name" ],
-        "sha256": [ "sha256", "size", "name" ],
-    }
-
-    __size_field_behavior = "apt-ftparchive"
-    def set_size_field_behavior(self, value):
-        if value not in ["apt-ftparchive", "dak"]:
-            raise ValueError("size_field_behavior must be either "
-                             "'apt-ftparchive' or 'dak'")
-        else:
-            self.__size_field_behavior = value
-    size_field_behavior = property(lambda self: self.__size_field_behavior,
-                                   set_size_field_behavior)
-
-    @property
-    def _fixed_field_lengths(self):
-        fixed_field_lengths = {}
-        for key in self._multivalued_fields:
-            length = self._get_size_field_length(key)
-            fixed_field_lengths[key] = {"size": length}
-        return fixed_field_lengths
-
-    def _get_size_field_length(self, key):
-        if self.size_field_behavior == "apt-ftparchive":
-            return 16
-        elif self.size_field_behavior == "dak":
-            lengths = [len(str(item['size'])) for item in self[key]]
-            return max(lengths)
-
-
-class Sources(Dsc, _PkgRelationMixin):
-    """Represent an APT source package list"""
-
-    _relationship_fields = [ 'build-depends', 'build-depends-indep',
-            'build-conflicts', 'build-conflicts-indep', 'binary' ]
-
-    def __init__(self, *args, **kwargs):
-        Dsc.__init__(self, *args, **kwargs)
-        _PkgRelationMixin.__init__(self, *args, **kwargs)
-
-
-class Packages(Deb822, _PkgRelationMixin):
-    """Represent an APT binary package list"""
-
-    _relationship_fields = [ 'depends', 'pre-depends', 'recommends',
-            'suggests', 'breaks', 'conflicts', 'provides', 'replaces',
-            'enhances' ]
-
-    def __init__(self, *args, **kwargs):
-        Deb822.__init__(self, *args, **kwargs)
-        _PkgRelationMixin.__init__(self, *args, **kwargs)
-
-###
-
-class _CaseInsensitiveString(str):
-    """Case insensitive string.
-    """
-
-    def __new__(cls, str_):
-        s = str.__new__(cls, str_)
-        s.str_lower = str_.lower()
-        s.str_lower_hash = hash(s.str_lower)
-        return s
-
-    def __hash__(self):
-        return self.str_lower_hash
-
-    def __eq__(self, other):
-        return self.str_lower == other.lower()
-
-    def lower(self):
-        return self.str_lower
-
-_strI = _CaseInsensitiveString
diff --git a/debian_bundle/debfile.py b/debian_bundle/debfile.py
deleted file mode 100644
index a2a62f6..0000000
--- a/debian_bundle/debfile.py
+++ /dev/null
@@ -1,282 +0,0 @@
-# DebFile: a Python representation of Debian .deb binary packages.
-# Copyright (C) 2007-2008   Stefano Zacchiroli  <zack at debian.org>
-# Copyright (C) 2007        Filippo Giunchedi   <filippo at debian.org>
-#
-# 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
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program 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
-# General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import gzip
-import tarfile
-
-from arfile import ArFile, ArError
-from changelog import Changelog
-from deb822 import Deb822
-
-DATA_PART = 'data.tar'      # w/o extension
-CTRL_PART = 'control.tar'
-PART_EXTS = ['gz', 'bz2']   # possible extensions
-INFO_PART = 'debian-binary'
-MAINT_SCRIPTS = ['preinst', 'postinst', 'prerm', 'postrm', 'config']
-
-CONTROL_FILE = 'control'
-CHANGELOG_NATIVE = 'usr/share/doc/%s/changelog.gz'  # with package stem
-CHANGELOG_DEBIAN = 'usr/share/doc/%s/changelog.Debian.gz'
-MD5_FILE = 'md5sums'
-
-
-class DebError(ArError):
-    pass
-
-
-class DebPart(object):
-    """'Part' of a .deb binary package.
-    
-    A .deb package is considered as made of 2 parts: a 'data' part
-    (corresponding to the 'data.tar.gz' archive embedded in a .deb) and a
-    'control' part (the 'control.tar.gz' archive). Each of them is represented
-    by an instance of this class. Each archive should be a compressed tar
-    archive; supported compression formats are: .tar.gz, .tar.bz2 .
-
-    When referring to file members of the underlying .tar.gz archive, file
-    names can be specified in one of 3 formats "file", "./file", "/file". In
-    all cases the file is considered relative to the root of the archive. For
-    the control part the preferred mechanism is the first one (as in
-    deb.control.get_content('control') ); for the data part the preferred
-    mechanism is the third one (as in deb.data.get_file('/etc/vim/vimrc') ).
-    """
-
-    def __init__(self, member):
-        self.__member = member  # arfile.ArMember file member
-        self.__tgz = None
-
-    def tgz(self):
-        """Return a TarFile object corresponding to this part of a .deb
-        package.
-        
-        Despite the name, this method gives access to various kind of
-        compressed tar archives, not only gzipped ones.
-        """
-
-        if self.__tgz is None:
-            name = self.__member.name
-            if name.endswith('.gz'):
-                gz = gzip.GzipFile(fileobj=self.__member, mode='r')
-                self.__tgz = tarfile.TarFile(fileobj=gz, mode='r')
-            elif name.endswith('.bz2'):
-                # Tarfile's __init__ doesn't allow for r:bz2 modes, but the
-                # open() classmethod does ...
-                self.__tgz = tarfile.open(fileobj=self.__member, mode='r:bz2')
-            else:
-                raise DebError("part '%s' has unexpected extension" % name)
-        return self.__tgz
-
-    @staticmethod
-    def __normalize_member(fname):
-        """ try (not so hard) to obtain a member file name in a form relative
-        to the .tar.gz root and with no heading '.' """
-
-        if fname.startswith('./'):
-            fname = fname[2:]
-        elif fname.startswith('/'):
-            fname = fname[1:]
-        return fname
-
-    # XXX in some of the following methods, compatibility among >= 2.5 and <<
-    # 2.5 python versions had to be taken into account. TarFile << 2.5 indeed
-    # was buggied and returned member file names with an heading './' only for
-    # the *first* file member. TarFile >= 2.5 fixed this and has the heading
-    # './' for all file members.
-
-    def has_file(self, fname):
-        """Check if this part contains a given file name."""
-
-        fname = DebPart.__normalize_member(fname)
-        names = self.tgz().getnames()
-        return (('./' + fname in names) \
-                or (fname in names)) # XXX python << 2.5 TarFile compatibility
-
-    def get_file(self, fname):
-        """Return a file object corresponding to a given file name."""
-
-        fname = DebPart.__normalize_member(fname)
-        try:
-            return (self.tgz().extractfile('./' + fname))
-        except KeyError:    # XXX python << 2.5 TarFile compatibility
-            return (self.tgz().extractfile(fname))
-
-    def get_content(self, fname):
-        """Return the string content of a given file, or None (e.g. for
-        directories)."""
-
-        f = self.get_file(fname)
-        content = None
-        if f:   # can be None for non regular or link files
-            content = f.read()
-            f.close()
-        return content
-
-    # container emulation
-
-    def __iter__(self):
-        return iter(self.tgz().getnames())
-
-    def __contains__(self, fname):
-        return self.has_file(fname)
-
-    def has_key(self, fname):
-        return self.has_file(fname)
-
-    def __getitem__(self, fname):
-        return self.get_content(fname)
-
-
-class DebData(DebPart):
-
-    pass
-
-
-class DebControl(DebPart):
-
-    def scripts(self):
-        """ Return a dictionary of maintainer scripts (postinst, prerm, ...)
-        mapping script names to script text. """
-
-        scripts = {}
-        for fname in MAINT_SCRIPTS:
-            if self.has_file(fname):
-                scripts[fname] = self.get_content(fname)
-
-        return scripts
-
-    def debcontrol(self):
-        """ Return the debian/control as a Deb822 (a Debian-specific dict-like
-        class) object.
-        
-        For a string representation of debian/control try
-        .get_content('control') """
-
-        return Deb822(self.get_content(CONTROL_FILE))
-
-    def md5sums(self):
-        """ Return a dictionary mapping filenames (of the data part) to
-        md5sums. Fails if the control part does not contain a 'md5sum' file.
-
-        Keys of the returned dictionary are the left-hand side values of lines
-        in the md5sums member of control.tar.gz, usually file names relative to
-        the file system root (without heading '/' or './'). """
-
-        if not self.has_file(MD5_FILE):
-            raise DebError("'%s' file not found, can't list MD5 sums" %
-                    MD5_FILE)
-
-        md5_file = self.get_file(MD5_FILE)
-        sums = {}
-        for line in md5_file.readlines():
-            # we need to support spaces in filenames, .split() is not enough
-            md5, fname = line.rstrip('\r\n').split(None, 1)
-            sums[fname] = md5
-        md5_file.close()
-        return sums
-
-
-class DebFile(ArFile):
-    """Representation of a .deb file (a Debian binary package)
-
-    DebFile objects have the following (read-only) properties:
-        - version       debian .deb file format version (not related with the
-                        contained package version), 2.0 at the time of writing
-                        for all .deb packages in the Debian archive
-        - data          DebPart object corresponding to the data.tar.gz (or
-                        other compressed tar) archive contained in the .deb
-                        file
-        - control       DebPart object corresponding to the control.tar.gz (or
-                        other compressed tar) archive contained in the .deb
-                        file
-    """
-
-    def __init__(self, filename=None, mode='r', fileobj=None):
-        ArFile.__init__(self, filename, mode, fileobj)
-        actual_names = set(self.getnames())
-
-        def compressed_part_name(basename):
-            global PART_EXTS
-            candidates = [ '%s.%s' % (basename, ext) for ext in PART_EXTS ]
-            parts = actual_names.intersection(set(candidates))
-            if not parts:
-                raise DebError("missing required part in given .deb" \
-                        " (expected one of: %s)" % candidates)
-            elif len(parts) > 1:
-                raise DebError("too many parts in given .deb" \
-                        " (was looking for only one of: %s)" % candidates)
-            else:   # singleton list
-                return list(parts)[0]
-
-        if not INFO_PART in actual_names:
-            raise DebError("missing required part in given .deb" \
-                    " (expected: '%s')" % INFO_PART)
-
-        self.__parts = {}
-        self.__parts[CTRL_PART] = DebControl(self.getmember(
-                compressed_part_name(CTRL_PART)))
-        self.__parts[DATA_PART] = DebData(self.getmember(
-                compressed_part_name(DATA_PART)))
-        self.__pkgname = None   # updated lazily by __updatePkgName
-
-        f = self.getmember(INFO_PART)
-        self.__version = f.read().strip()
-        f.close()
-
-    def __updatePkgName(self):
-        self.__pkgname = self.debcontrol()['package']
-
-    version = property(lambda self: self.__version)
-    data = property(lambda self: self.__parts[DATA_PART])
-    control = property(lambda self: self.__parts[CTRL_PART])
-
-    # proxy methods for the appropriate parts
-
-    def debcontrol(self):
-        """ See .control.debcontrol() """
-        return self.control.debcontrol()
-
-    def scripts(self):
-        """ See .control.scripts() """
-        return self.control.scripts()
-
-    def md5sums(self):
-        """ See .control.md5sums() """
-        return self.control.md5sums()
-
-    def changelog(self):
-        """ Return a Changelog object for the changelog.Debian.gz of the
-        present .deb package. Return None if no changelog can be found. """
-
-        if self.__pkgname is None:
-            self.__updatePkgName()
-
-        for fname in [ CHANGELOG_DEBIAN % self.__pkgname,
-                CHANGELOG_NATIVE % self.__pkgname ]:
-            if self.data.has_file(fname):
-                gz = gzip.GzipFile(fileobj=self.data.get_file(fname))
-                raw_changelog = gz.read()
-                gz.close()
-                return Changelog(raw_changelog)
-        return None
-
-
-if __name__ == '__main__':
-    import sys
-    deb = DebFile(filename=sys.argv[1])
-    tgz = deb.control.tgz()
-    print tgz.getmember('control')
-
diff --git a/debian_bundle/debian_support.py b/debian_bundle/debian_support.py
deleted file mode 100644
index d216bba..0000000
--- a/debian_bundle/debian_support.py
+++ /dev/null
@@ -1,652 +0,0 @@
-# debian_support.py -- Python module for Debian metadata
-# Copyright (C) 2005 Florian Weimer <fw at deneb.enyo.de>
-# Copyright (C) 2010 John Wright <jsw at debian.org>
-# 
-# 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
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-# 
-# This program 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 General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-"""This module implements facilities to deal with Debian-specific metadata."""
-
-import os
-import re
-import hashlib
-import types
-
-from deprecation import function_deprecated_by
-
-try:
-    import apt_pkg
-    apt_pkg.init()
-    __have_apt_pkg = True
-except ImportError:
-    __have_apt_pkg = False
-
-class ParseError(Exception):
-    """An exception which is used to signal a parse failure.
-
-    Attributes:
-
-    filename - name of the file
-    lineno - line number in the file
-    msg - error message
-
-    """
-    
-    def __init__(self, filename, lineno, msg):
-        assert type(lineno) == types.IntType
-        self.filename = filename
-        self.lineno = lineno
-        self.msg = msg
-
-    def __str__(self):
-        return self.msg
-
-    def __repr__(self):
-        return "ParseError(%s, %d, %s)" % (`self.filename`,
-                                           self.lineno,
-                                           `self.msg`)
-
-    def print_out(self, file):
-        """Writes a machine-parsable error message to file."""
-        file.write("%s:%d: %s\n" % (self.filename, self.lineno, self.msg))
-        file.flush()
-
-    printOut = function_deprecated_by(print_out)
-
-class BaseVersion(object):
-    """Base class for classes representing Debian versions
-
-    It doesn't implement any comparison, but it does check for valid versions
-    according to Section 5.6.12 in the Debian Policy Manual.  Since splitting
-    the version into epoch, upstream_version, and debian_revision components is
-    pretty much free with the validation, it sets those fields as properties of
-    the object, and sets the raw version to the full_version property.  A
-    missing epoch or debian_revision results in the respective property set to
-    None.  Setting any of the properties results in the full_version being
-    recomputed and the rest of the properties set from that.
-
-    It also implements __str__, just returning the raw version given to the
-    initializer.
-    """
-
-    re_valid_version = re.compile(
-            r"^((?P<epoch>\d+):)?"
-             "(?P<upstream_version>[A-Za-z0-9.+:~-]+?)"
-             "(-(?P<debian_revision>[A-Za-z0-9+.~]+))?$")
-    magic_attrs = ('full_version', 'epoch', 'upstream_version',
-                   'debian_revision', 'debian_version')
-
-    def __init__(self, version):
-        self.full_version = version
-
-    def _set_full_version(self, version):
-        m = self.re_valid_version.match(version)
-        if not m:
-            raise ValueError("Invalid version string %r" % version)
-
-        self.__full_version = version
-        self.__epoch = m.group("epoch")
-        self.__upstream_version = m.group("upstream_version")
-        self.__debian_revision = m.group("debian_revision")
-
-    def __setattr__(self, attr, value):
-        if attr not in self.magic_attrs:
-            super(BaseVersion, self).__setattr__(attr, value)
-            return
-
-        # For compatibility with the old changelog.Version class
-        if attr == "debian_version":
-            attr = "debian_revision"
-
-        if attr == "full_version":
-            self._set_full_version(str(value))
-        else:
-            if value is not None:
-                value = str(value)
-            private = "_BaseVersion__%s" % attr
-            old_value = getattr(self, private)
-            setattr(self, private, value)
-            try:
-                self._update_full_version()
-            except ValueError:
-                # Don't leave it in an invalid state
-                setattr(self, private, old_value)
-                self._update_full_version()
-                raise ValueError("Setting %s to %r results in invalid version"
-                                 % (attr, value))
-
-    def __getattr__(self, attr):
-        if attr not in self.magic_attrs:
-            return super(BaseVersion, self).__getattribute__(attr)
-
-        # For compatibility with the old changelog.Version class
-        if attr == "debian_version":
-            attr = "debian_revision"
-
-        private = "_BaseVersion__%s" % attr
-        return getattr(self, private)
-
-    def _update_full_version(self):
-        version = ""
-        if self.__epoch is not None:
-            version += self.__epoch + ":"
-        version += self.__upstream_version
-        if self.__debian_revision:
-            version += "-" + self.__debian_revision
-        self.full_version = version
-
-    def __str__(self):
-        return self.full_version
-
-    def __repr__(self):
-        return "%s('%s')" % (self.__class__.__name__, self)
-
-    def __cmp__(self, other):
-        raise NotImplementedError
-
-    def __hash__(self):
-        return hash(str(self))
-
-class AptPkgVersion(BaseVersion):
-    """Represents a Debian package version, using apt_pkg.VersionCompare"""
-
-    def __cmp__(self, other):
-        return apt_pkg.VersionCompare(str(self), str(other))
-
-# NativeVersion based on the DpkgVersion class by Raphael Hertzog in
-# svn://svn.debian.org/qa/trunk/pts/www/bin/common.py r2361
-class NativeVersion(BaseVersion):
-    """Represents a Debian package version, with native Python comparison"""
-
-    re_all_digits_or_not = re.compile("\d+|\D+")
-    re_digits = re.compile("\d+")
-    re_digit = re.compile("\d")
-    re_alpha = re.compile("[A-Za-z]")
-
-    def __cmp__(self, other):
-        # Convert other into an instance of BaseVersion if it's not already.
-        # (All we need is epoch, upstream_version, and debian_revision
-        # attributes, which BaseVersion gives us.) Requires other's string
-        # representation to be the raw version.
-        if not isinstance(other, BaseVersion):
-            try:
-                other = BaseVersion(str(other))
-            except ValueError, e:
-                raise ValueError("Couldn't convert %r to BaseVersion: %s"
-                                 % (other, e))
-
-        res = cmp(int(self.epoch or "0"), int(other.epoch or "0"))
-        if res != 0:
-            return res
-        res = self._version_cmp_part(self.upstream_version,
-                                     other.upstream_version)
-        if res != 0:
-            return res
-        return self._version_cmp_part(self.debian_revision or "0",
-                                      other.debian_revision or "0")
-
-    @classmethod
-    def _order(cls, x):
-        """Return an integer value for character x"""
-        if x == '~':
-            return -1
-        elif cls.re_digit.match(x):
-            return int(x) + 1
-        elif cls.re_alpha.match(x):
-            return ord(x)
-        else:
-            return ord(x) + 256
-
-    @classmethod
-    def _version_cmp_string(cls, va, vb):
-        la = [cls._order(x) for x in va]
-        lb = [cls._order(x) for x in vb]
-        while la or lb:
-            a = 0
-            b = 0
-            if la:
-                a = la.pop(0)
-            if lb:
-                b = lb.pop(0)
-            res = cmp(a, b)
-            if res != 0:
-                return res
-        return 0
-
-    @classmethod
-    def _version_cmp_part(cls, va, vb):
-        la = cls.re_all_digits_or_not.findall(va)
-        lb = cls.re_all_digits_or_not.findall(vb)
-        while la or lb:
-            a = "0"
-            b = "0"
-            if la:
-                a = la.pop(0)
-            if lb:
-                b = lb.pop(0)
-            if cls.re_digits.match(a) and cls.re_digits.match(b):
-                a = int(a)
-                b = int(b)
-                res = cmp(a, b)
-                if res != 0:
-                    return res
-            else:
-                res = cls._version_cmp_string(a, b)
-                if res != 0:
-                    return res
-        return 0
-
-if __have_apt_pkg:
-    class Version(AptPkgVersion):
-        pass
-else:
-    class Version(NativeVersion):
-        pass
-
-def version_compare(a, b):
-    return cmp(Version(a), Version(b))
-
-class PackageFile:
-    """A Debian package file.
-
-    Objects of this class can be used to read Debian's Source and
-    Packages files."""
-
-    re_field = re.compile(r'^([A-Za-z][A-Za-z0-9-]+):(?:\s*(.*?))?\s*$')
-    re_continuation = re.compile(r'^\s+(?:\.|(\S.*?)\s*)$')
-
-    def __init__(self, name, file_obj=None):
-        """Creates a new package file object.
-
-        name - the name of the file the data comes from
-        file_obj - an alternate data source; the default is to open the
-                  file with the indicated name.
-        """
-        if file_obj is None:
-            file_obj = file(name)
-        self.name = name
-        self.file = file_obj
-        self.lineno = 0
-
-    def __iter__(self):
-        line = self.file.readline()
-        self.lineno += 1
-        pkg = []
-        while line:
-            if line.strip(' \t') == '\n':
-                if len(pkg) == 0:
-                    self.raise_syntax_error('expected package record')
-                yield pkg
-                pkg = []
-                line = self.file.readline()
-                self.lineno += 1
-                continue
-            
-            match = self.re_field.match(line)
-            if not match:
-                self.raise_syntax_error("expected package field")
-            (name, contents) = match.groups()
-            contents = contents or ''
-
-            while True:
-                line = self.file.readline()
-                self.lineno += 1
-                match = self.re_continuation.match(line)
-                if match:
-                    (ncontents,) = match.groups()
-                    if ncontents is None:
-                        ncontents = ""
-                    contents = "%s\n%s" % (contents, ncontents)
-                else:
-                    break
-            pkg.append((name, contents))
-        if pkg:
-            yield pkg
-
-    def raise_syntax_error(self, msg, lineno=None):
-        if lineno is None:
-            lineno = self.lineno
-        raise ParseError(self.name, lineno, msg)
-
-    raiseSyntaxError = function_deprecated_by(raise_syntax_error)
-
-class PseudoEnum:
-    """A base class for types which resemble enumeration types."""
-    def __init__(self, name, order):
-        self._name = name
-        self._order = order
-    def __repr__(self):
-        return '%s(%s)'% (self.__class__._name__, `name`)
-    def __str__(self):
-        return self._name
-    def __cmp__(self, other):
-        return cmp(self._order, other._order)
-    def __hash__(self):
-        return hash(self._order)
-
-class Release(PseudoEnum): pass
-
-def list_releases():
-    releases = {}
-    rels = ("potato", "woody", "sarge", "etch", "lenny", "sid")
-    for r in range(len(rels)):
-        releases[rels[r]] = Release(rels[r], r)
-    Release.releases = releases
-    return releases
-
-listReleases = function_deprecated_by(list_releases)
-
-def intern_release(name, releases=list_releases()):
-    if releases.has_key(name):
-        return releases[name]
-    else:
-        return None
-
-internRelease = function_deprecated_by(intern_release)
-
-del listReleases
-del list_releases
-
-def read_lines_sha1(lines):
-    m = hashlib.sha1()
-    for l in lines:
-        m.update(l)
-    return m.hexdigest()
-
-readLinesSHA1 = function_deprecated_by(read_lines_sha1)
-
-def patches_from_ed_script(source,
-                        re_cmd=re.compile(r'^(\d+)(?:,(\d+))?([acd])$')):
-    """Converts source to a stream of patches.
-
-    Patches are triples of line indexes:
-
-    - number of the first line to be replaced
-    - one plus the number of the last line to be replaced
-    - list of line replacements
-
-    This is enough to model arbitrary additions, deletions and
-    replacements.
-    """
-
-    i = iter(source)
-    
-    for line in i:
-        match = re_cmd.match(line)
-        if match is None:
-            raise ValueError, "invalid patch command: " + `line`
-
-        (first, last, cmd) = match.groups()
-        first = int(first)
-        if last is not None:
-            last = int(last)
-
-        if cmd == 'd':
-            first = first - 1
-            if last is None:
-                last = first + 1
-            yield (first, last, [])
-            continue
-
-        if cmd == 'a':
-            if last is not None:
-                raise ValueError, "invalid patch argument: " + `line`
-            last = first
-        else:                           # cmd == c
-            first = first - 1
-            if last is None:
-                last = first + 1
-
-        lines = []
-        for l in i:
-            if l == '':
-                raise ValueError, "end of stream in command: " + `line`
-            if l == '.\n' or l == '.':
-                break
-            lines.append(l)
-        yield (first, last, lines)
-
-patchesFromEdScript = function_deprecated_by(patches_from_ed_script)
-
-def patch_lines(lines, patches):
-    """Applies patches to lines.  Updates lines in place."""
-    for (first, last, args) in patches:
-        lines[first:last] = args
-
-patchLines = function_deprecated_by(patch_lines)
-
-def replace_file(lines, local):
-
-    import os.path
-
-    local_new = local + '.new'
-    new_file = file(local_new, 'w+')
-
-    try:
-        for l in lines:
-            new_file.write(l)
-        new_file.close()
-        os.rename(local_new, local)
-    finally:
-        if os.path.exists(local_new):
-            os.unlink(local_new)
-
-replaceFile = function_deprecated_by(replace_file)
-
-def download_gunzip_lines(remote):
-    """Downloads a file from a remote location and gunzips it.
-
-    Returns the lines in the file."""
-
-    # The implementation is rather crude, but it seems that the gzip
-    # module needs a real file for input.
-
-    import gzip
-    import tempfile
-    import urllib
-
-    (handle, fname) = tempfile.mkstemp()
-    try:
-        os.close(handle)
-        (filename, headers) = urllib.urlretrieve(remote, fname)
-        gfile = gzip.GzipFile(filename)
-        lines = gfile.readlines()
-        gfile.close()
-    finally:
-        os.unlink(fname)
-    return lines
-
-downloadGunzipLines = function_deprecated_by(download_gunzip_lines)
-
-def download_file(remote, local):
-    """Copies a gzipped remote file to the local system.
-
-    remote - URL, without the .gz suffix
-    local - name of the local file
-    """
-    
-    lines = download_gunzip_lines(remote + '.gz')
-    replace_file(lines, local)
-    return lines
-
-downloadFile = function_deprecated_by(download_file)
-
-def update_file(remote, local, verbose=None):
-    """Updates the local file by downloading a remote patch.
-
-    Returns a list of lines in the local file.
-    """
-
-    try:
-        local_file = file(local)
-    except IOError:
-        if verbose:
-            print "update_file: no local copy, downloading full file"
-        return download_file(remote, local)
-
-    lines = local_file.readlines()
-    local_file.close()
-    local_hash = read_lines_sha1(lines)
-    patches_to_apply = []
-    patch_hashes = {}
-    
-    import urllib
-    index_name = remote + '.diff/Index'
-
-    re_whitespace=re.compile('\s+')
-
-    try:
-        index_url = urllib.urlopen(index_name)
-        index_fields = list(PackageFile(index_name, index_url))
-    except ParseError:
-        # FIXME: urllib does not raise a proper exception, so we parse
-        # the error message.
-        if verbose:
-            print "update_file: could not interpret patch index file"
-        return download_file(remote, local)
-    except IOError:
-        if verbose:
-            print "update_file: could not download patch index file"
-        return download_file(remote, local)
-
-    for fields in index_fields:
-        for (field, value) in fields:
-            if field == 'SHA1-Current':
-                (remote_hash, remote_size) = re_whitespace.split(value)
-                if local_hash == remote_hash:
-                    if verbose:
-                        print "update_file: local file is up-to-date"
-                    return lines
-                continue
-
-            if field =='SHA1-History':
-                for entry in value.splitlines():
-                    if entry == '':
-                        continue
-                    (hist_hash, hist_size, patch_name) \
-                                = re_whitespace.split(entry)
-
-                    # After the first patch, we have to apply all
-                    # remaining patches.
-                    if patches_to_apply or  hist_hash == local_hash:
-                        patches_to_apply.append(patch_name)
-                        
-                continue
-            
-            if field == 'SHA1-Patches':
-                for entry in value.splitlines():
-                    if entry == '':
-                        continue
-                    (patch_hash, patch_size, patch_name) \
-                                 = re_whitespace.split(entry)
-                    patch_hashes[patch_name] = patch_hash
-                continue
-            
-            if verbose:
-                print "update_file: field %s ignored" % `field`
-        
-    if not patches_to_apply:
-        if verbose:
-            print "update_file: could not find historic entry", local_hash
-        return download_file(remote, local)
-
-    for patch_name in patches_to_apply:
-        print "update_file: downloading patch " + `patch_name`
-        patch_contents = download_gunzip_lines(remote + '.diff/' + patch_name
-                                          + '.gz')
-        if read_lines_sha1(patch_contents ) <> patch_hashes[patch_name]:
-            raise ValueError, "patch %s was garbled" % `patch_name`
-        patch_lines(lines, patches_from_ed_script(patch_contents))
-        
-    new_hash = read_lines_sha1(lines)
-    if new_hash <> remote_hash:
-        raise ValueError, ("patch failed, got %s instead of %s"
-                           % (new_hash, remote_hash))
-
-    replace_file(lines, local)
-    return lines
-
-updateFile = function_deprecated_by(update_file)
-
-def merge_as_sets(*args):
-    """Create an order set (represented as a list) of the objects in
-    the sequences passed as arguments."""
-    s = {}
-    for x in args:
-        for y in x:
-            s[y] = True
-    l = s.keys()
-    l.sort()
-    return l
-
-mergeAsSets = function_deprecated_by(merge_as_sets)
-
-def test():
-    # Version
-    for (cls1, cls2) in [(AptPkgVersion, AptPkgVersion),
-                         (AptPkgVersion, NativeVersion),
-                         (NativeVersion, AptPkgVersion),
-                         (NativeVersion, NativeVersion),
-                         (str, AptPkgVersion), (AptPkgVersion, str),
-                         (str, NativeVersion), (NativeVersion, str)]:
-        assert cls1('0') < cls2('a')
-        assert cls1('1.0') < cls2('1.1')
-        assert cls1('1.2') < cls2('1.11')
-        assert cls1('1.0-0.1') < cls2('1.1')
-        assert cls1('1.0-0.1') < cls2('1.0-1')
-        assert cls1('1.0') == cls2('1.0')
-        assert cls1('1.0-0.1') == cls2('1.0-0.1')
-        assert cls1('1:1.0-0.1') == cls2('1:1.0-0.1')
-        assert cls1('1:1.0') == cls2('1:1.0')
-        assert cls1('1.0-0.1') < cls2('1.0-1')
-        assert cls1('1.0final-5sarge1') > cls2('1.0final-5') \
-               > cls2('1.0a7-2')
-        assert cls1('0.9.2-5') < cls2('0.9.2+cvs.1.0.dev.2004.07.28-1.5')
-        assert cls1('1:500') < cls2('1:5000')
-        assert cls1('100:500') > cls2('11:5000')
-        assert cls1('1.0.4-2') > cls2('1.0pre7-2')
-        assert cls1('1.5~rc1') < cls2('1.5')
-        assert cls1('1.5~rc1') < cls2('1.5+b1')
-        assert cls1('1.5~rc1') < cls2('1.5~rc2')
-        assert cls1('1.5~rc1') > cls2('1.5~dev0')
-
-    # Release
-    assert intern_release('sarge') < intern_release('etch')
-
-    # PackageFile
-    # for p in PackageFile('../../data/packages/sarge/Sources'):
-    #     assert p[0][0] == 'Package'
-    # for p in PackageFile('../../data/packages/sarge/Packages.i386'):
-    #     assert p[0][0] == 'Package'
-
-    # Helper routines
-    assert read_lines_sha1([]) == 'da39a3ee5e6b4b0d3255bfef95601890afd80709'
-    assert read_lines_sha1(['1\n', '23\n']) \
-           == '14293c9bd646a15dc656eaf8fba95124020dfada'
-
-    file_a = map(lambda x: "%d\n" % x, range(1, 18))
-    file_b = ['0\n', '1\n', '<2>\n', '<3>\n', '4\n', '5\n', '7\n', '8\n',
-              '11\n', '12\n', '<13>\n', '14\n', '15\n', 'A\n', 'B\n', 'C\n',
-              '16\n', '17\n',]
-    patch = ['15a\n', 'A\n', 'B\n', 'C\n', '.\n', '13c\n', '<13>\n', '.\n',
-             '9,10d\n', '6d\n', '2,3c\n', '<2>\n', '<3>\n', '.\n', '0a\n',
-             '0\n', '.\n']
-    patch_lines(file_a, patches_from_ed_script(patch))
-    assert ''.join(file_b) == ''.join(file_a)
-
-    assert len(merge_as_sets([])) == 0
-    assert ''.join(merge_as_sets("abc", "cb")) == "abc"
-
-if __name__ == "__main__":
-    test()
diff --git a/debian_bundle/debtags.py b/debian_bundle/debtags.py
deleted file mode 100644
index cc44f14..0000000
--- a/debian_bundle/debtags.py
+++ /dev/null
@@ -1,505 +0,0 @@
-
-# debtags.py -- Access and manipulate Debtags information
-# Copyright (C) 2006-2007  Enrico Zini <enrico at enricozini.org>
-#
-# 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
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program 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
-# General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import re, cPickle
-
-from deprecation import function_deprecated_by
-
-def parse_tags(input):
-	lre = re.compile(r"^(.+?)(?::?\s*|:\s+(.+?)\s*)$")
-	for line in input:
-		# Is there a way to remove the last character of a line that does not
-		# make a copy of the entire line?
-		m = lre.match(line)
-		pkgs = set(m.group(1).split(', '))
-		if m.group(2):
-			tags = set(m.group(2).split(', '))
-		else:
-			tags = set()
-		yield pkgs, tags
-
-parseTags = function_deprecated_by(parse_tags)
-
-def read_tag_database(input):
-	"Read the tag database, returning a pkg->tags dictionary"
-	db = {}
-	for pkgs, tags in parse_tags(input):
-		# Create the tag set using the native set
-		for p in pkgs:
-			db[p] = tags.copy()
-	return db;
-
-readTagDatabase = function_deprecated_by(read_tag_database)
-
-def read_tag_database_reversed(input):
-	"Read the tag database, returning a tag->pkgs dictionary"
-	db = {}
-	for pkgs, tags in parse_tags(input):
-		# Create the tag set using the native set
-		for tag in tags:
-			if db.has_key(tag):
-				db[tag] |= pkgs
-			else:
-				db[tag] = pkgs.copy()
-	return db;
-
-readTagDatabaseReversed = function_deprecated_by(read_tag_database_reversed)
-
-def read_tag_database_both_ways(input, tag_filter = None):
-	"Read the tag database, returning a pkg->tags and a tag->pkgs dictionary"
-	db = {}
-	dbr = {}
-	for pkgs, tags in parse_tags(input):
-		# Create the tag set using the native set
-		if tag_filter == None:
-			tags = set(tags)
-		else:
-			tags = set(filter(tag_filter, tags))
-		for pkg in pkgs:
-			db[pkg] = tags.copy()
-		for tag in tags:
-			if dbr.has_key(tag):
-				dbr[tag] |= pkgs
-			else:
-				dbr[tag] = pkgs.copy()
-	return db, dbr;
-
-readTagDatabaseBothWays = function_deprecated_by(read_tag_database_both_ways)
-
-def reverse(db):
-	"Reverse a tag database, from package -> tags to tag->packages"
-	res = {}
-	for pkg, tags in db.items():
-		for tag in tags:
-			if not res.has_key(tag):
-				res[tag] = set()
-			res[tag].add(pkg)
-	return res
-
-
-def output(db):
-	"Write the tag database"
-	for pkg, tags in db.items():
-		# Using % here seems awkward to me, but if I use calls to
-		# sys.stdout.write it becomes a bit slower
-		print "%s:" % (pkg), ", ".join(tags)
-
-
-def relevance_index_function(full, sub):
-	#return (float(sub.card(tag)) / float(sub.tag_count())) / \
-	#       (float(full.card(tag)) / float(full.tag_count()))
-	#return sub.card(tag) * full.card(tag) / sub.tag_count()
-
-	# New cardinality divided by the old cardinality
-	#return float(sub.card(tag)) / float(full.card(tag))
-
-	## Same as before, but weighted by the relevance the tag had in the
-	## full collection, to downplay the importance of rare tags
-	#return float(sub.card(tag) * full.card(tag)) / float(full.card(tag) * full.tag_count())
-	# Simplified version:
-	#return float(sub.card(tag)) / float(full.tag_count())
-	
-	# Weighted by the square root of the relevance, to downplay the very
-	# common tags a bit
-	#return lambda tag: float(sub.card(tag)) / float(full.card(tag)) * math.sqrt(full.card(tag) / float(full.tag_count()))
-	#return lambda tag: float(sub.card(tag)) / float(full.card(tag)) * math.sqrt(full.card(tag) / float(full.package_count()))
-	# One useless factor removed, and simplified further, thanks to Benjamin Mesing
-	return lambda tag: float(sub.card(tag)**2) / float(full.card(tag))
-
-	# The difference between how many packages are in and how many packages are out
-	# (problems: tags that mean many different things can be very much out
-	# as well.  In the case of 'image editor', for example, there will be
-	# lots of editors not for images in the outside group.
-	# It is very, very good for nonambiguous keywords like 'image'.
-	#return lambda tag: 2 * sub.card(tag) - full.card(tag)
-	# Same but it tries to downplay the 'how many are out' value in the
-	# case of popular tags, to mitigate the 'there will always be popular
-	# tags left out' cases.  Does not seem to be much of an improvement.
-	#return lambda tag: sub.card(tag) - float(full.card(tag) - sub.card(tag))/(math.sin(float(full.card(tag))*3.1415/full.package_count())/4 + 0.75)
-
-relevanceIndexFunction = function_deprecated_by(relevance_index_function)
-
-class DB:
-	"""
-	In-memory database mapping packages to tags and tags to packages.
-	"""
-
-	def __init__(self):
-		self.db = {}
-		self.rdb = {}
-	
-	def read(self, input, tag_filter=None):
-		"""
-		Read the database from a file.
-
-		Example::
-			# Read the system Debtags database
-			db.read(open("/var/lib/debtags/package-tags", "r"))
-		"""
-		self.db, self.rdb = read_tag_database_both_ways(input, tag_filter)
-
-	def qwrite(self, file):
-		"Quickly write the data to a pickled file"
-		cPickle.dump(self.db, file)
-		cPickle.dump(self.rdb, file)
-
-	def qread(self, file):
-		"Quickly read the data from a pickled file"
-		self.db = cPickle.load(file)
-		self.rdb = cPickle.load(file)
-
-	def insert(self, pkg, tags):
-		self.db[pkg] = tags.copy()
-		for tag in tags:
-			if self.rdb.has_key(tag):
-				self.rdb[tag].add(pkg)
-			else:
-				self.rdb[tag] = set((pkg))
-
-	def dump(self):
-		output(self.db)
-
-	def dump_reverse(self):
-		output(self.rdb)
-
-	dumpReverse = function_deprecated_by(dump_reverse)
-	
-	def reverse(self):
-		"Return the reverse collection, sharing tagsets with this one"
-		res = DB()
-		res.db = self.rdb
-		res.rdb = self.db
-		return res
-
-	def facet_collection(self):
-		"""
-		Return a copy of this collection, but replaces the tag names
-		with only their facets.
-		"""
-		fcoll = DB()
-		tofacet = re.compile(r"^([^:]+).+")
-		for pkg, tags in self.iter_packagesTags():
-			ftags = set([tofacet.sub(r"\1", t) for t in tags])
-			fcoll.insert(pkg, ftags)
-		return fcoll
-
-	facetCollection = function_deprecated_by(facet_collection)
-
-	def copy(self):
-		"""
-		Return a copy of this collection, with the tagsets copied as
-		well.
-		"""
-		res = DB()
-		res.db = self.db.copy()
-		res.rdb = self.rdb.copy()
-		return res
-
-	def reverse_copy(self):
-		"""
-		Return the reverse collection, with a copy of the tagsets of
-		this one.
-		"""
-		res = DB()
-		res.db = self.rdb.copy()
-		res.rdb = self.db.copy()
-		return res
-
-	reverseCopy = function_deprecated_by(reverse_copy)
-
-	def choose_packages(self, package_iter):
-		"""
-		Return a collection with only the packages in package_iter,
-		sharing tagsets with this one
-		"""
-		res = DB()
-		db = {}
-		for pkg in package_iter:
-			if self.db.has_key(pkg): db[pkg] = self.db[pkg]
-		res.db = db
-		res.rdb = reverse(db)
-		return res
-
-	choosePackages = function_deprecated_by(choose_packages)
-
-	def choose_packages_copy(self, package_iter):
-		"""
-		Return a collection with only the packages in package_iter,
-		with a copy of the tagsets of this one
-		"""
-		res = DB()
-		db = {}
-		for pkg in package_iter:
-			db[pkg] = self.db[pkg]
-		res.db = db
-		res.rdb = reverse(db)
-		return res
-
-	choosePackagesCopy = function_deprecated_by(choose_packages_copy)
-
-	def filter_packages(self, package_filter):
-		"""
-		Return a collection with only those packages that match a
-		filter, sharing tagsets with this one.  The filter will match
-		on the package.
-		"""
-		res = DB()
-		db = {}
-		for pkg in filter(package_filter, self.db.iterkeys()):
-			db[pkg] = self.db[pkg]
-		res.db = db
-		res.rdb = reverse(db)
-		return res
-
-	filterPackages = function_deprecated_by(filter_packages)
-
-	def filter_packages_copy(self, filter):
-		"""
-		Return a collection with only those packages that match a
-		filter, with a copy of the tagsets of this one.  The filter
-		will match on the package.
-		"""
-		res = DB()
-		db = {}
-		for pkg in filter(filter, self.db.iterkeys()):
-			db[pkg] = self.db[pkg].copy()
-		res.db = db
-		res.rdb = reverse(db)
-		return res
-
-	filterPackagesCopy = function_deprecated_by(filter_packages_copy)
-
-	def filter_packages_tags(self, package_tag_filter):
-		"""
-		Return a collection with only those packages that match a
-		filter, sharing tagsets with this one.  The filter will match
-		on (package, tags).
-		"""
-		res = DB()
-		db = {}
-		for pkg, tags in filter(package_tag_filter, self.db.iteritems()):
-			db[pkg] = self.db[pkg]
-		res.db = db
-		res.rdb = reverse(db)
-		return res
-
-	filterPackagesTags = function_deprecated_by(filter_packages_tags)
-
-	def filter_packages_tags_copy(self, package_tag_filter):
-		"""
-		Return a collection with only those packages that match a
-		filter, with a copy of the tagsets of this one.  The filter
-		will match on (package, tags).
-		"""
-		res = DB()
-		db = {}
-		for pkg, tags in filter(package_tag_filter, self.db.iteritems()):
-			db[pkg] = self.db[pkg].copy()
-		res.db = db
-		res.rdb = reverse(db)
-		return res
-
-	filterPackagesTagsCopy = function_deprecated_by(filter_packages_tags_copy)
-
-	def filter_tags(self, tag_filter):
-		"""
-		Return a collection with only those tags that match a
-		filter, sharing package sets with this one.  The filter will match
-		on the tag.
-		"""
-		res = DB()
-		rdb = {}
-		for tag in filter(tag_filter, self.rdb.iterkeys()):
-			rdb[tag] = self.rdb[tag]
-		res.rdb = rdb
-		res.db = reverse(rdb)
-		return res
-
-	filterTags = function_deprecated_by(filter_tags)
-
-	def filter_tags_copy(self, tag_filter):
-		"""
-		Return a collection with only those tags that match a
-		filter, with a copy of the package sets of this one.  The
-		filter will match on the tag.
-		"""
-		res = DB()
-		rdb = {}
-		for tag in filter(tag_filter, self.rdb.iterkeys()):
-			rdb[tag] = self.rdb[tag].copy()
-		res.rdb = rdb
-		res.db = reverse(rdb)
-		return res
-
-	filterTagsCopy = function_deprecated_by(filter_tags_copy)
-
-	def has_package(self, pkg):
-		"""Check if the collection contains the given package"""
-		return self.db.has_key(pkg)
-
-	hasPackage = function_deprecated_by(has_package)
-
-	def has_tag(self, tag):
-		"""Check if the collection contains packages tagged with tag"""
-		return self.rdb.has_key(tag)
-
-	hasTag = function_deprecated_by(has_tag)
-
-	def tags_of_package(self, pkg):
-		"""Return the tag set of a package"""
-		return self.db.has_key(pkg) and self.db[pkg] or set()
-
-	tagsOfPackage = function_deprecated_by(tags_of_package)
-
-	def packages_of_tag(self, tag):
-		"""Return the package set of a tag"""
-		return self.rdb.has_key(tag) and self.rdb[tag] or set()
-
-	packagesOfTag = function_deprecated_by(packages_of_tag)
-
-	def tags_of_packages(self, pkgs):
-		"""Return the set of tags that have all the packages in pkgs"""
-		res = None
-		for p in pkgs:
-			if res == None:
-				res = set(self.tags_of_package(p))
-			else:
-				res &= self.tags_of_package(p)
-		return res
-
-	tagsOfPackages = function_deprecated_by(tags_of_packages)
-
-	def packages_of_tags(self, tags):
-		"""Return the set of packages that have all the tags in tags"""
-		res = None
-		for t in tags:
-			if res == None:
-				res = set(self.packages_of_tag(t))
-			else:
-				res &= self.packages_of_tag(t)
-		return res
-
-	packagesOfTags = function_deprecated_by(packages_of_tags)
-
-	def card(self, tag):
-		"""
-		Return the cardinality of a tag
-		"""
-		return self.rdb.has_key(tag) and len(self.rdb[tag]) or 0
-
-	def discriminance(self, tag):
-		"""
-		Return the discriminance index if the tag.
-		
-		Th discriminance index of the tag is defined as the minimum
-		number of packages that would be eliminated by selecting only
-		those tagged with this tag or only those not tagged with this
-		tag.
-		"""
-		n = self.card(tag)
-		tot = self.package_count()
-		return min(n, tot - n)
-
-	def iter_packages(self):
-		"""Iterate over the packages"""
-		return self.db.iterkeys()
-
-	iterPackages = function_deprecated_by(iter_packages)
-
-	def iter_tags(self):
-		"""Iterate over the tags"""
-		return self.rdb.iterkeys()
-
-	iterTags = function_deprecated_by(iter_tags)
-
-	def iter_packages_tags(self):
-		"""Iterate over 2-tuples of (pkg, tags)"""
-		return self.db.iteritems()
-
-	iterPackagesTags = function_deprecated_by(iter_packages_tags)
-
-	def iter_tags_packages(self):
-		"""Iterate over 2-tuples of (tag, pkgs)"""
-		return self.rdb.iteritems()
-
-	iterTagsPackages = function_deprecated_by(iter_tags_packages)
-
-	def package_count(self):
-		"""Return the number of packages"""
-		return len(self.db)
-
-	packageCount = function_deprecated_by(package_count)
-
-	def tag_count(self):
-		"""Return the number of tags"""
-		return len(self.rdb)
-
-	tagCount = function_deprecated_by(tag_count)
-
-	def ideal_tagset(self, tags):
-		"""
-		Return an ideal selection of the top tags in a list of tags.
-
-		Return the tagset made of the highest number of tags taken in
-		consecutive sequence from the beginning of the given vector,
-		that would intersecate with the tagset of a comfortable amount
-		of packages.
-
-		Comfortable is defined in terms of how far it is from 7.
-		"""
-
-		# TODO: the scoring function is quite ok, but may need more
-		# tuning.  I also center it on 15 instead of 7 since we're
-		# setting a starting point for the search, not a target point
-		def score_fun(x):
-			return float((x-15)*(x-15))/x
-
-		hits = []
-		tagset = set()
-		min_score = 3
-		for i in range(len(tags)):
-			pkgs = self.packages_of_tags(tags[:i+1])
-			card = len(pkgs)
-			if card == 0: break;
-			score = score_fun(card)
-			if score < min_score:
-				min_score = score
-				tagset = set(tags[:i+1])
-
-		# Return always at least the first tag
-		if len(tagset) == 0:
-			return set(tags[:1])
-		else:
-			return tagset
-
-	idealTagset = function_deprecated_by(ideal_tagset)
-
-	def correlations(self):
-		"""
-		Generate the list of correlation as a tuple (hastag, hasalsotag, score).
-
-		Every touple will indicate that the tag 'hastag' tends to also
-		have 'hasalsotag' with a score of 'score'.
-		"""
-		for pivot in self.iter_tags():
-			with_ = self.filter_packages_tags(lambda pt: pivot in pt[1])
-			without = self.filter_packages_tags(lambda pt: pivot not in pt[1])
-			for tag in with_.iter_tags():
-				if tag == pivot: continue
-				has = float(with_.card(tag)) / float(with_.package_count())
-				hasnt = float(without.card(tag)) / float(without.package_count())
-				yield pivot, tag, has - hasnt
diff --git a/debian_bundle/deprecation.py b/debian_bundle/deprecation.py
deleted file mode 100644
index 6ed7e5a..0000000
--- a/debian_bundle/deprecation.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# -*- coding: utf-8 -*- vim: fileencoding=utf-8 :
-#
-# debian_bundle/deprecation.py
-# Utility module to deprecate features
-#
-# Copyright © Ben Finney <ben+debian at benfinney.id.au>
-#
-# 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 the Free Software Foundation, either version 2
-# of the License, or (at your option) any later version.
-#
-# This program 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 General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-
-""" Utility module to deprecate features """
-
-import warnings
-
-def function_deprecated_by(func):
-    """ Return a function that warns it is deprecated by another function.
-
-        Returns a new function that warns it is deprecated by function
-        ``func``, then acts as a pass-through wrapper for ``func``.
-
-        """
-    func_name = func.__name__
-    warn_msg = "Use %(func_name)s instead" % vars()
-    def deprecated_func(*args, **kwargs):
-        warnings.warn(warn_msg, DeprecationWarning, stacklevel=2)
-        return func(*args, **kwargs)
-    return deprecated_func
diff --git a/debian_bundle/doc-debtags b/debian_bundle/doc-debtags
deleted file mode 100755
index fecc77f..0000000
--- a/debian_bundle/doc-debtags
+++ /dev/null
@@ -1,98 +0,0 @@
-#!/usr/bin/python
-
-import debtags
-import sys
-import inspect
-
-def print_indented (spaces, string):
-	for line in string.split("\n"):
-		for i in range(1,spaces):
-			sys.stdout.write(" ")
-		sys.stdout.write(line)
-		sys.stdout.write("\n")
-
-def document (callable):
-	if callable.__doc__ != None:
-		print_indented(2, callable.__name__)
-		print_indented(4, inspect.getdoc(callable))
-		print
-
-
-print """debtags.py README
-=================
-
-The Debtags python module provides support for accessing and manipulating
-Debtags tag data.
-
-The module provides a single class, debtags.DB, which implements various kinds
-of tag operations on an in-memory tag database.
-
-The database can be queried both as a database of packages with associated tags
-and as a database of tags with associated packages.  Performance are good in
-both ways: querying the tags of a package has the same peed as querying the
-packages having a tag.
-
-debtags.DB allows both simple queries and more complex algorithms to be
-implemented easily and efficiently.  Have a look at the Sample usage section
-below for some examples.
-
-
-Classes
-=======
-
-There is only one class: debtags.DB:
-"""
-
-document (debtags.DB)
-
-print """
-The methods of debtags.DB are:
-"""
-
-for m in dir(debtags.DB):
-	if m[0:2] != '__' and callable(getattr(debtags.DB, m)):
-		document(getattr(debtags.DB, m))
-
-print """Iteration
-=========
-
-debtags.DB provides various iteration methods to iterate the collection either
-in a package-centered or in a tag-centered way:
-"""
-
-document(debtags.DB.iter_packages)
-document(debtags.DB.iter_packages_tags)
-document(debtags.DB.iter_tags)
-document(debtags.DB.iter_tags_packages)
-
-
-print """Sample usage
-============
-
-This example reads the system debtags database and performs a simple tag
-search::
-
-    import debtags
-    
-    db = debtags.DB()
-    db.read(open("/var/lib/debtags/package-tags", "r"))
-    print db.package_count(), "packages in the database"
-    print "Image editors:"
-    for pkg in db.packages_of_tags(set(("use::editing", "works-with::image:raster"))):
-    	print " *", pkg
-
-This example computes the set of tags that belong to all the packages in a
-list, then shows all the other packages that have those tags:
-
-    import debtags
-
-    db = debtags.DB()
-    db.read(open("/var/lib/debtags/package-tags", "r"))
-    tags = db.tags_of_packages(("gimp", "krita"))
-    print "Common tags:"
-    for tag in tags:
-	print " *", tag
-    print "Packages similar to gimp and krita:"
-    for pkg in db.packages_of_tags(tags):
-	print " *", pkg
-"""
diff --git a/lib/deb822.py b/lib/deb822.py
new file mode 100644
index 0000000..6dc74ec
--- /dev/null
+++ b/lib/deb822.py
@@ -0,0 +1,6 @@
+import sys
+print >> sys.stderr, "WARNING:",    \
+        "the 'deb822' top-level module is *DEPRECATED*,",   \
+        "please use 'debian.deb822'"
+
+from debian.deb822 import *
diff --git a/lib/debian/__init__.py b/lib/debian/__init__.py
new file mode 100644
index 0000000..b28b04f
--- /dev/null
+++ b/lib/debian/__init__.py
@@ -0,0 +1,3 @@
+
+
+
diff --git a/lib/debian/arfile.py b/lib/debian/arfile.py
new file mode 100644
index 0000000..9ad757e
--- /dev/null
+++ b/lib/debian/arfile.py
@@ -0,0 +1,314 @@
+# ArFile: a Python representation of ar (as in "man 1 ar") archives.
+# Copyright (C) 2007    Stefano Zacchiroli  <zack at debian.org>
+# Copyright (C) 2007    Filippo Giunchedi   <filippo at debian.org>
+#
+# 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
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+GLOBAL_HEADER = "!<arch>\n"
+GLOBAL_HEADER_LENGTH = len(GLOBAL_HEADER)
+
+FILE_HEADER_LENGTH = 60
+FILE_MAGIC = "`\n"
+
+class ArError(Exception):
+    pass
+
+class ArFile(object):
+    """ Representation of an ar archive, see man 1 ar.
+    
+    The interface of this class tries to mimick that of the TarFile module in
+    the standard library.
+    
+    ArFile objects have the following (read-only) properties:
+        - members       same as getmembers()
+    """
+
+    def __init__(self, filename=None, mode='r', fileobj=None):
+        """ Build an ar file representation starting from either a filename or
+        an existing file object. The only supported mode is 'r' """
+
+        self.__members = [] 
+        self.__members_dict = {}
+        self.__fname = filename
+        self.__fileobj = fileobj
+        
+        if mode == "r":
+            self.__index_archive()
+        pass    # TODO write support
+
+    def __index_archive(self):
+        if self.__fname:
+            fp = open(self.__fname, "rb")
+        elif self.__fileobj:
+            fp = self.__fileobj
+        else:
+            raise ArError, "Unable to open valid file"
+
+        if fp.read(GLOBAL_HEADER_LENGTH) != GLOBAL_HEADER:
+            raise ArError, "Unable to find global header"
+
+        while True:
+            newmember = ArMember.from_file(fp, self.__fname)
+            if not newmember:
+                break
+            self.__members.append(newmember)
+            self.__members_dict[newmember.name] = newmember
+            if newmember.size % 2 == 0: # even, no padding
+                fp.seek(newmember.size, 1) # skip to next header
+            else:
+                fp.seek(newmember.size + 1 , 1) # skip to next header
+        
+        if self.__fname:
+            fp.close()
+
+    def getmember(self, name):
+        """ Return the (last occurrence of a) member in the archive whose name
+        is 'name'. Raise KeyError if no member matches the given name.
+
+        Note that in case of name collisions the only way to retrieve all
+        members matching a given name is to use getmembers. """
+
+        return self.__members_dict[name]
+
+    def getmembers(self):
+        """ Return a list of all members contained in the archive.
+
+        The list has the same order of members in the archive and can contain
+        duplicate members (i.e. members with the same name) if they are
+        duplicate in the archive itself. """
+
+        return self.__members
+
+    members = property(getmembers)
+
+    def getnames(self):
+        """ Return a list of all member names in the archive. """
+
+        return map(lambda f: f.name, self.__members)
+
+    def extractall():
+        """ Not (yet) implemented. """
+
+        raise NotImpelementedError  # TODO
+
+    def extract(self, member, path):
+        """ Not (yet) implemented. """
+
+        raise NotImpelementedError  # TODO
+
+    def extractfile(self, member):
+        """ Return a file object corresponding to the requested member. A member
+        can be specified either as a string (its name) or as a ArMember
+        instance. """
+
+        for m in self.__members:
+            if isinstance(member, ArMember) and m.name == member.name:
+                return m
+            elif member == m.name:
+                return m
+            else:
+                return None 
+
+    # container emulation
+
+    def __iter__(self):
+        """ Iterate over the members of the present ar archive. """
+
+        return iter(self.__members)
+
+    def __getitem__(self, name):
+        """ Same as .getmember(name). """
+
+        return self.getmember(name)
+
+
+class ArMember(object):
+    """ Member of an ar archive.
+
+    Implements most of a file object interface: read, readline, next,
+    readlines, seek, tell, close.
+    
+    ArMember objects have the following (read-only) properties:
+        - name      member name in an ar archive
+        - mtime     modification time
+        - owner     owner user
+        - group     owner group
+        - fmode     file permissions
+        - size      size in bytes
+        - fname     file name"""
+
+    def __init__(self):
+        self.__name = None      # member name (i.e. filename) in the archive
+        self.__mtime = None     # last modification time
+        self.__owner = None     # owner user
+        self.__group = None     # owner group
+        self.__fmode = None     # permissions
+        self.__size = None      # member size in bytes
+        self.__fname = None     # file name associated with this member
+        self.__fp = None        # file pointer 
+        self.__offset = None    # start-of-data offset
+        self.__end = None       # end-of-data offset
+
+    def from_file(fp, fname):
+        """fp is an open File object positioned on a valid file header inside
+        an ar archive. Return a new ArMember on success, None otherwise. """
+
+        buf = fp.read(FILE_HEADER_LENGTH)
+
+        if not buf:
+            return None
+
+        # sanity checks
+        if len(buf) < FILE_HEADER_LENGTH:
+            raise IOError, "Incorrect header length"
+
+        if buf[58:60] != FILE_MAGIC:
+            raise IOError, "Incorrect file magic"
+
+        # http://en.wikipedia.org/wiki/Ar_(Unix)    
+        #from   to     Name                      Format
+        #0      15     File name                 ASCII
+        #16     27     File modification date    Decimal
+        #28     33     Owner ID                  Decimal
+        #34     39     Group ID                  Decimal
+        #40     47     File mode                 Octal
+        #48     57     File size in bytes        Decimal
+        #58     59     File magic                \140\012
+
+        # XXX struct.unpack can be used as well here
+        f = ArMember()
+        f.__name = buf[0:16].split("/")[0].strip()
+        f.__mtime = int(buf[16:28])
+        f.__owner = int(buf[28:34])
+        f.__group = int(buf[34:40])
+        f.__fmode  = buf[40:48]  # XXX octal value
+        f.__size  = int(buf[48:58])
+
+        f.__fname = fname
+        f.__offset = fp.tell() # start-of-data
+        f.__end = f.__offset + f.__size
+
+        return f
+
+    from_file = staticmethod(from_file)
+    
+    # file interface
+
+    # XXX this is not a sequence like file objects
+    def read(self, size=0):
+        if self.__fp is None:
+            self.__fp = open(self.__fname, "r")
+            self.__fp.seek(self.__offset)
+
+        cur = self.__fp.tell()
+
+        if size > 0 and size <= self.__end - cur: # there's room
+            return self.__fp.read(size)
+
+        if cur >= self.__end or cur < self.__offset:
+            return ''
+
+        return self.__fp.read(self.__end - cur)
+
+    def readline(self, size=None):
+        if self.__fp is None:
+            self.__fp = open(self.__fname, "r")
+            self.__fp.seek(self.__offset)
+
+        if size is not None: 
+            buf = self.__fp.readline(size)
+            if self.__fp.tell() > self.__end:
+                return ''
+
+            return buf
+
+        buf = self.__fp.readline()
+        if self.__fp.tell() > self.__end:
+            return ''
+        else:
+            return buf
+
+    def readlines(self, sizehint=0):
+        if self.__fp is None:
+            self.__fp = open(self.__fname, "r")
+            self.__fp.seek(self.__offset)
+        
+        buf = None
+        lines = []
+        while True: 
+            buf = self.readline()
+            if not buf: 
+                break
+            lines.append(buf)
+
+        return lines
+
+    def seek(self, offset, whence=0):
+        if self.__fp is None:
+            self.__fp = open(self.__fname, "r")
+            self.__fp.seek(self.__offset)
+
+        if self.__fp.tell() < self.__offset:
+            self.__fp.seek(self.__offset)
+
+        if whence < 2 and offset + self.__fp.tell() < self.__offset:
+            raise IOError, "Can't seek at %d" % offset
+        
+        if whence == 1:
+            self.__fp.seek(offset, 1)
+        elif whence == 0:
+            self.__fp.seek(self.__offset + offset, 0)
+        elif whence == 2:
+            self.__fp.seek(self.__end + offset, 0)
+
+    def tell(self):
+        if self.__fp is None:
+            self.__fp = open(self.__fname, "r")
+            self.__fp.seek(self.__offset)
+
+        cur = self.__fp.tell()
+        
+        if cur < self.__offset:
+            return 0L
+        else:
+            return cur - self.__offset
+
+    def close(self):
+        if self.__fp is not None:
+            self.__fp.close()
+   
+    def next(self):
+        return self.readline()
+    
+    def __iter__(self):
+        def nextline():
+            line = self.readline()
+            if line:
+                yield line
+
+        return iter(nextline())
+
+    name = property(lambda self: self.__name)
+    mtime = property(lambda self: self.__mtime)
+    owner = property(lambda self: self.__owner)
+    group = property(lambda self: self.__group)
+    fmode = property(lambda self: self.__fmode)
+    size = property(lambda self: self.__size)
+    fname = property(lambda self: self.__fname)
+
+if __name__ == '__main__':
+    # test
+    # ar r test.ar <file1> <file2> .. <fileN>
+    a = ArFile("test.ar")
+    print "\n".join(a.getnames())
diff --git a/lib/debian/changelog.py b/lib/debian/changelog.py
new file mode 100644
index 0000000..9ef7c49
--- /dev/null
+++ b/lib/debian/changelog.py
@@ -0,0 +1,472 @@
+# 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
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# 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 re
+import warnings
+
+import debian_support
+
+class ChangelogParseError(StandardError):
+    """Indicates that the changelog could not be parsed"""
+    is_user_error = True
+
+    def __init__(self, line):
+        self._line=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"""
+
+class VersionError(StandardError):
+    """Indicates that the version does not conform to the required format"""
+
+    is_user_error = True
+
+    def __init__(self, version):
+        self._version=version
+
+    def __str__(self):
+        return "Could not parse version: "+self._version
+
+class Version(debian_support.Version):
+    """Represents a version of a Debian package."""
+    # debian_support.Version now has all the functionality we need
+
+class ChangeBlock(object):
+    """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
+
+    def changes(self):
+        return self._changes
+
+    def add_trailing_line(self, line):
+        self._trailing.append(line)
+
+    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._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(r'^(\w%(name_chars)s*) \(([^\(\) \t]+)\)'
+                     '((\s+%(name_chars)s+)+)\;'
+                     % {'name_chars': '[-+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*$')
+
+
+class Changelog(object):
+    """Represents a debian/changelog file. You can ask it several things
+       about the file.
+    """
+
+
+    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):
+            # Make sure the changelog file is not empty.
+            if file is None or len(file.strip()) == 0:
+                self._parse_error('Empty changelog file.', strict)
+                return
+
+            file = file.splitlines()
+        for line in file:
+            # Support both lists of lines without the trailing newline and
+            # those with trailing newlines (e.g. when given a file object
+            # directly)
+            line = line.rstrip('\n')
+            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_revision)
+    debian_revision = property(lambda self: self.version.debian_revision)
+    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
+
+    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]
+
+    versions = property(get_versions,
+                      doc="List of version objects the package went through")
+
+    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 __iter__(self):
+        return iter(self._blocks)
+
+    def __len__(self):
+        return len(self._blocks)
+
+    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 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_date(self, date):
+        self._blocks[0].date = date
+    date = property(lambda self: self._blocks[0].date, set_date)
+
+    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__())
diff --git a/lib/debian/deb822.py b/lib/debian/deb822.py
new file mode 100644
index 0000000..15eb056
--- /dev/null
+++ b/lib/debian/deb822.py
@@ -0,0 +1,1106 @@
+# vim: fileencoding=utf-8
+#
+# A python interface for various rfc822-like formatted files used by Debian
+# (.changes, .dsc, Packages, Sources, etc)
+#
+# Copyright (C) 2005-2006  dann frazier <dannf at dannf.org>
+# Copyright (C) 2006-2008  John Wright <john at johnwright.org>
+# Copyright (C) 2006       Adeodato Simó <dato at net.com.org.es>
+# Copyright (C) 2008       Stefano Zacchiroli <zack at upsilon.cc>
+#
+# 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 the Free Software Foundation, either version 2
+# of the License, or (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+from deprecation import function_deprecated_by
+
+try:
+    import apt_pkg
+    _have_apt_pkg = True
+except ImportError:
+    _have_apt_pkg = False
+
+import new
+import re
+import string
+import sys
+import StringIO
+import UserDict
+
+class TagSectionWrapper(object, UserDict.DictMixin):
+    """Wrap a TagSection object, using its find_raw method to get field values
+
+    This allows us to pick which whitespace to strip off the beginning and end
+    of the data, so we don't lose leading newlines.
+    """
+
+    def __init__(self, section):
+        self.__section = section
+
+    def keys(self):
+        return self.__section.keys()
+
+    def __getitem__(self, key):
+        s = self.__section.find_raw(key)
+
+        if s is None:
+            raise KeyError(key)
+
+        # Get just the stuff after the first ':'
+        # Could use s.partition if we only supported python >= 2.5
+        data = s[s.find(':')+1:]
+
+        # Get rid of spaces and tabs after the ':', but not newlines, and strip
+        # off any newline at the end of the data.
+        return data.lstrip(' \t').rstrip('\n')
+
+class OrderedSet(object):
+    """A set-like object that preserves order when iterating over it
+
+    We use this to keep track of keys in Deb822Dict, because it's much faster
+    to look up if a key is in a set than in a list.
+    """
+
+    def __init__(self, iterable=[]):
+        self.__set = set()
+        self.__order = []
+        for item in iterable:
+            self.add(item)
+
+    def add(self, item):
+        if item not in self:
+            # set.add will raise TypeError if something's unhashable, so we
+            # don't have to handle that ourselves
+            self.__set.add(item)
+            self.__order.append(item)
+
+    def remove(self, item):
+        # set.remove will raise KeyError, so we don't need to handle that
+        # ourselves
+        self.__set.remove(item)
+        self.__order.remove(item)
+
+    def __iter__(self):
+        # Return an iterator of items in the order they were added
+        return iter(self.__order)
+
+    def __contains__(self, item):
+        # This is what makes OrderedSet faster than using a list to keep track
+        # of keys.  Lookup in a set is O(1) instead of O(n) for a list.
+        return item in self.__set
+
+    ### list-like methods
+    append = add
+
+    def extend(self, iterable):
+        for item in iterable:
+            self.add(item)
+    ###
+
+class Deb822Dict(object, UserDict.DictMixin):
+    # Subclassing UserDict.DictMixin because we're overriding so much dict
+    # functionality that subclassing dict requires overriding many more than
+    # the four methods that DictMixin requires.
+    """A dictionary-like object suitable for storing RFC822-like data.
+
+    Deb822Dict behaves like a normal dict, except:
+        - key lookup is case-insensitive
+        - key order is preserved
+        - if initialized with a _parsed parameter, it will pull values from
+          that dictionary-like object as needed (rather than making a copy).
+          The _parsed dict is expected to be able to handle case-insensitive
+          keys.
+
+    If _parsed is not None, an optional _fields parameter specifies which keys
+    in the _parsed dictionary are exposed.
+    """
+
+    # See the end of the file for the definition of _strI
+
+    def __init__(self, _dict=None, _parsed=None, _fields=None,
+                 encoding="utf-8"):
+        self.__dict = {}
+        self.__keys = OrderedSet()
+        self.__parsed = None
+        self.encoding = encoding
+
+        if _dict is not None:
+            # _dict may be a dict or a list of two-sized tuples
+            if hasattr(_dict, 'items'):
+                items = _dict.items()
+            else:
+                items = list(_dict)
+
+            try:
+                for k, v in items:
+                    self[k] = v
+            except ValueError:
+                this = len(self.__keys)
+                len_ = len(items[this])
+                raise ValueError('dictionary update sequence element #%d has '
+                    'length %d; 2 is required' % (this, len_))
+        
+        if _parsed is not None:
+            self.__parsed = _parsed
+            if _fields is None:
+                self.__keys.extend([ _strI(k) for k in self.__parsed.keys() ])
+            else:
+                self.__keys.extend([ _strI(f) for f in _fields if self.__parsed.has_key(f) ])
+        
+    ### BEGIN DictMixin methods
+
+    def __setitem__(self, key, value):
+        key = _strI(key)
+        self.__keys.add(key)
+        self.__dict[key] = value
+        
+    def __getitem__(self, key):
+        key = _strI(key)
+        try:
+            value = self.__dict[key]
+        except KeyError:
+            if self.__parsed is not None and key in self.__keys:
+                value = self.__parsed[key]
+            else:
+                raise
+
+        if isinstance(value, str):
+            # Always return unicode objects instead of strings
+            value = value.decode(self.encoding)
+        return value
+
+    def __delitem__(self, key):
+        key = _strI(key)
+        self.__keys.remove(key)
+        try:
+            del self.__dict[key]
+        except KeyError:
+            # If we got this far, the key was in self.__keys, so it must have
+            # only been in the self.__parsed dict.
+            pass
+
+    def has_key(self, key):
+        key = _strI(key)
+        return key in self.__keys
+    
+    def keys(self):
+        return [str(key) for key in self.__keys]
+    
+    ### END DictMixin methods
+
+    def __repr__(self):
+        return '{%s}' % ', '.join(['%r: %r' % (k, v) for k, v in self.items()])
+
+    def __eq__(self, other):
+        mykeys = self.keys(); mykeys.sort()
+        otherkeys = other.keys(); otherkeys.sort()
+        if not mykeys == otherkeys:
+            return False
+
+        for key in mykeys:
+            if self[key] != other[key]:
+                return False
+
+        # If we got here, everything matched
+        return True
+
+    def copy(self):
+        # Use self.__class__ so this works as expected for subclasses
+        copy = self.__class__(self)
+        return copy
+
+    # TODO implement __str__() and make dump() use that?
+
+
+class Deb822(Deb822Dict):
+
+    def __init__(self, sequence=None, fields=None, _parsed=None,
+                 encoding="utf-8"):
+        """Create a new Deb822 instance.
+
+        :param sequence: a string, or any any object that returns a line of
+            input each time, normally a file().  Alternately, sequence can
+            be a dict that contains the initial key-value pairs.
+
+        :param fields: if given, it is interpreted as a list of fields that
+            should be parsed (the rest will be discarded).
+
+        :param _parsed: internal parameter.
+
+        :param encoding: When parsing strings, interpret them in this encoding.
+            (All values are given back as unicode objects, so an encoding is
+            necessary in order to properly interpret the strings.)
+        """
+
+        if hasattr(sequence, 'items'):
+            _dict = sequence
+            sequence = None
+        else:
+            _dict = None
+        Deb822Dict.__init__(self, _dict=_dict, _parsed=_parsed, _fields=fields,
+                            encoding=encoding)
+
+        if sequence is not None:
+            try:
+                self._internal_parser(sequence, fields)
+            except EOFError:
+                pass
+
+        self.gpg_info = None
+
+    def iter_paragraphs(cls, sequence, fields=None, use_apt_pkg=True,
+                        shared_storage=False, encoding="utf-8"):
+        """Generator that yields a Deb822 object for each paragraph in sequence.
+
+        :param sequence: same as in __init__.
+
+        :param fields: likewise.
+
+        :param use_apt_pkg: if sequence is a file(), apt_pkg will be used 
+            if available to parse the file, since it's much much faster.  Set
+            this parameter to False to disable using apt_pkg.
+        :param shared_storage: not used, here for historical reasons.  Deb822
+            objects never use shared storage anymore.
+        :param encoding: Interpret the paragraphs in this encoding.
+            (All values are given back as unicode objects, so an encoding is
+            necessary in order to properly interpret the strings.)
+        """
+
+        if _have_apt_pkg and use_apt_pkg and isinstance(sequence, file):
+            parser = apt_pkg.TagFile(sequence)
+            for section in parser:
+                yield cls(fields=fields, _parsed=TagSectionWrapper(section),
+                          encoding=encoding)
+
+        else:
+            iterable = iter(sequence)
+            x = cls(iterable, fields, encoding=encoding)
+            while len(x) != 0:
+                yield x
+                x = cls(iterable, fields, encoding=encoding)
+
+    iter_paragraphs = classmethod(iter_paragraphs)
+
+    ###
+
+    def _internal_parser(self, sequence, fields=None):
+        single = re.compile("^(?P<key>\S+)\s*:\s*(?P<data>\S.*?)\s*$")
+        multi = re.compile("^(?P<key>\S+)\s*:\s*$")
+        multidata = re.compile("^\s(?P<data>.+?)\s*$")
+
+        wanted_field = lambda f: fields is None or f in fields
+
+        if isinstance(sequence, basestring):
+            sequence = sequence.splitlines()
+
+        curkey = None
+        content = ""
+        for line in self.gpg_stripped_paragraph(sequence):
+            if isinstance(line, str):
+                line = line.decode(self.encoding)
+            m = single.match(line)
+            if m:
+                if curkey:
+                    self[curkey] += content
+
+                if not wanted_field(m.group('key')):
+                    curkey = None
+                    continue
+
+                curkey = m.group('key')
+                self[curkey] = m.group('data')
+                content = ""
+                continue
+
+            m = multi.match(line)
+            if m:
+                if curkey:
+                    self[curkey] += content
+
+                if not wanted_field(m.group('key')):
+                    curkey = None
+                    continue
+
+                curkey = m.group('key')
+                self[curkey] = ""
+                content = ""
+                continue
+
+            m = multidata.match(line)
+            if m:
+                content += '\n' + line # XXX not m.group('data')?
+                continue
+
+        if curkey:
+            self[curkey] += content
+
+    def __str__(self):
+        return self.dump()
+
+    def __unicode__(self):
+        return self.dump()
+
+    # __repr__ is handled by Deb822Dict
+
+    def get_as_string(self, key):
+        """Return the self[key] as a string (or unicode)
+
+        The default implementation just returns unicode(self[key]); however,
+        this can be overridden in subclasses (e.g. _multivalued) that can take
+        special values.
+        """
+        return unicode(self[key])
+
+    def dump(self, fd=None, encoding=None):
+        """Dump the the contents in the original format
+
+        If fd is None, return a unicode object.
+
+        If fd is not None, attempt to encode the output to the encoding the
+        object was initialized with, or the value of the encoding argument if
+        it is not None.  This will raise UnicodeEncodeError if the encoding
+        can't support all the characters in the Deb822Dict values.
+        """
+
+        if fd is None:
+            fd = StringIO.StringIO()
+            return_string = True
+        else:
+            return_string = False
+
+        if encoding is None:
+            # Use the encoding we've been using to decode strings with if none
+            # was explicitly specified
+            encoding = self.encoding
+
+        for key in self.iterkeys():
+            value = self.get_as_string(key)
+            if not value or value[0] == '\n':
+                # Avoid trailing whitespace after "Field:" if it's on its own
+                # line or the value is empty
+                # XXX Uh, really print value if value == '\n'?
+                entry = '%s:%s\n' % (key, value)
+            else:
+                entry = '%s: %s\n' % (key, value)
+            if not return_string:
+                fd.write(entry.encode(encoding))
+            else:
+                fd.write(entry)
+        if return_string:
+            return fd.getvalue()
+
+    ###
+
+    def is_single_line(self, s):
+        if s.count("\n"):
+            return False
+        else:
+            return True
+
+    isSingleLine = function_deprecated_by(is_single_line)
+
+    def is_multi_line(self, s):
+        return not self.is_single_line(s)
+
+    isMultiLine = function_deprecated_by(is_multi_line)
+
+    def _merge_fields(self, s1, s2):
+        if not s2:
+            return s1
+        if not s1:
+            return s2
+
+        if self.is_single_line(s1) and self.is_single_line(s2):
+            ## some fields are delimited by a single space, others
+            ## a comma followed by a space.  this heuristic assumes
+            ## that there are multiple items in one of the string fields
+            ## so that we can pick up on the delimiter being used
+            delim = ' '
+            if (s1 + s2).count(', '):
+                delim = ', '
+
+            L = (s1 + delim + s2).split(delim)
+            L.sort()
+
+            prev = merged = L[0]
+
+            for item in L[1:]:
+                ## skip duplicate entries
+                if item == prev:
+                    continue
+                merged = merged + delim + item
+                prev = item
+            return merged
+
+        if self.is_multi_line(s1) and self.is_multi_line(s2):
+            for item in s2.splitlines(True):
+                if item not in s1.splitlines(True):
+                    s1 = s1 + "\n" + item
+            return s1
+
+        raise ValueError
+
+    _mergeFields = function_deprecated_by(_merge_fields)
+
+    def merge_fields(self, key, d1, d2=None):
+        ## this method can work in two ways - abstract that away
+        if d2 == None:
+            x1 = self
+            x2 = d1
+        else:
+            x1 = d1
+            x2 = d2
+
+        ## we only have to do work if both objects contain our key
+        ## otherwise, we just take the one that does, or raise an
+        ## exception if neither does
+        if key in x1 and key in x2:
+            merged = self._mergeFields(x1[key], x2[key])
+        elif key in x1:
+            merged = x1[key]
+        elif key in x2:
+            merged = x2[key]
+        else:
+            raise KeyError
+
+        ## back to the two different ways - if this method was called
+        ## upon an object, update that object in place.
+        ## return nothing in this case, to make the author notice a
+        ## problem if she assumes the object itself will not be modified
+        if d2 == None:
+            self[key] = merged
+            return None
+
+        return merged
+
+    mergeFields = function_deprecated_by(merge_fields)
+
+    def split_gpg_and_payload(sequence):
+        """Return a (gpg_pre, payload, gpg_post) tuple
+
+        Each element of the returned tuple is a list of lines (with trailing
+        whitespace stripped).
+        """
+
+        gpg_pre_lines = []
+        lines = []
+        gpg_post_lines = []
+        state = 'SAFE'
+        gpgre = re.compile(r'^-----(?P<action>BEGIN|END) PGP (?P<what>[^-]+)-----$')
+        blank_line = re.compile('^$')
+        first_line = True
+
+        for line in sequence:
+            line = line.strip('\r\n')
+
+            # skip initial blank lines, if any
+            if first_line:
+                if blank_line.match(line):
+                    continue
+                else:
+                    first_line = False
+
+            m = gpgre.match(line)
+
+            if not m:
+                if state == 'SAFE':
+                    if not blank_line.match(line):
+                        lines.append(line)
+                    else:
+                        if not gpg_pre_lines:
+                            # There's no gpg signature, so we should stop at
+                            # this blank line
+                            break
+                elif state == 'SIGNED MESSAGE':
+                    if blank_line.match(line):
+                        state = 'SAFE'
+                    else:
+                        gpg_pre_lines.append(line)
+                elif state == 'SIGNATURE':
+                    gpg_post_lines.append(line)
+            else:
+                if m.group('action') == 'BEGIN':
+                    state = m.group('what')
+                elif m.group('action') == 'END':
+                    gpg_post_lines.append(line)
+                    break
+                if not blank_line.match(line):
+                    if not lines:
+                        gpg_pre_lines.append(line)
+                    else:
+                        gpg_post_lines.append(line)
+
+        if len(lines):
+            return (gpg_pre_lines, lines, gpg_post_lines)
+        else:
+            raise EOFError('only blank lines found in input')
+
+    split_gpg_and_payload = staticmethod(split_gpg_and_payload)
+
+    def gpg_stripped_paragraph(cls, sequence):
+        return cls.split_gpg_and_payload(sequence)[1]
+
+    gpg_stripped_paragraph = classmethod(gpg_stripped_paragraph)
+
+    def get_gpg_info(self):
+        """Return a GpgInfo object with GPG signature information
+
+        This method will raise ValueError if the signature is not available
+        (e.g. the original text cannot be found)"""
+
+        # raw_text is saved (as a string) only for Changes and Dsc (see
+        # _gpg_multivalued.__init__) which is small compared to Packages or
+        # Sources which contain no signature
+        if not hasattr(self, 'raw_text'):
+            raise ValueError, "original text cannot be found"
+
+        if self.gpg_info is None:
+            self.gpg_info = GpgInfo.from_sequence(self.raw_text)
+
+        return self.gpg_info
+
+###
+
+# XXX check what happens if input contains more that one signature
+class GpgInfo(dict):
+    """A wrapper around gnupg parsable output obtained via --status-fd
+
+    This class is really a dictionary containing parsed output from gnupg plus
+    some methods to make sense of the data.
+    Keys are keywords and values are arguments suitably splitted.
+    See /usr/share/doc/gnupg/DETAILS.gz"""
+
+    # keys with format "key keyid uid"
+    uidkeys = ('GOODSIG', 'EXPSIG', 'EXPKEYSIG', 'REVKEYSIG', 'BADSIG')
+
+    def valid(self):
+        """Is the signature valid?"""
+        return self.has_key('GOODSIG') or self.has_key('VALIDSIG')
+    
+# XXX implement as a property?
+# XXX handle utf-8 %-encoding
+    def uid(self):
+        """Return the primary ID of the signee key, None is not available"""
+        pass
+
+    @staticmethod
+    def from_output(out, err=None):
+        """Create a new GpgInfo object from gpg(v) --status-fd output (out) and
+        optionally collect stderr as well (err).
+        
+        Both out and err can be lines in newline-terminated sequence or regular strings."""
+
+        n = GpgInfo()
+
+        if isinstance(out, basestring):
+            out = out.split('\n')
+        if isinstance(err, basestring):
+            err = err.split('\n')
+
+        n.out = out
+        n.err = err
+        
+        header = '[GNUPG:] '
+        for l in out:
+            if not l.startswith(header):
+                continue
+
+            l = l[len(header):]
+            l = l.strip('\n')
+
+            # str.partition() would be better, 2.5 only though
+            s = l.find(' ')
+            key = l[:s]
+            if key in GpgInfo.uidkeys:
+                # value is "keyid UID", don't split UID
+                value = l[s+1:].split(' ', 1)
+            else:
+                value = l[s+1:].split(' ')
+
+            n[key] = value
+        return n 
+
+# XXX how to handle sequences of lines? file() returns \n-terminated
+    @staticmethod
+    def from_sequence(sequence, keyrings=['/usr/share/keyrings/debian-keyring.gpg'],
+            executable=["/usr/bin/gpgv"]):
+        """Create a new GpgInfo object from the given sequence.
+
+        Sequence is a sequence of lines or a string
+        executable is a list of args for subprocess.Popen, the first element being the gpg executable"""
+
+        # XXX check for gpg as well and use --verify accordingly?
+        args = executable
+        #args.extend(["--status-fd", "1", "--no-default-keyring"])
+        args.extend(["--status-fd", "1"])
+        import os
+        [args.extend(["--keyring", k]) for k in keyrings if os.path.isfile(k) and os.access(k, os.R_OK)]
+        
+        if "--keyring" not in args:
+            raise IOError, "cannot access none of given keyrings"
+
+        import subprocess
+        p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        # XXX what to do with exit code?
+
+        if isinstance(sequence, basestring):
+            (out, err) = p.communicate(sequence)
+        else:
+            (out, err) = p.communicate("\n".join(sequence))
+
+        return GpgInfo.from_output(out, err)
+
+    @staticmethod
+    def from_file(target, *args):
+        """Create a new GpgInfo object from the given file, calls from_sequence(file(target), *args)"""
+        return from_sequence(file(target), *args)
+    
+###
+
+class PkgRelation(object):
+    """Inter-package relationships
+
+    Structured representation of the relationships of a package to another,
+    i.e. of what can appear in a Deb882 field like Depends, Recommends,
+    Suggests, ... (see Debian Policy 7.1).
+    """
+
+    # XXX *NOT* a real dependency parser, and that is not even a goal here, we
+    # just parse as much as we need to split the various parts composing a
+    # dependency, checking their correctness wrt policy is out of scope
+    __dep_RE = re.compile( \
+            r'^\s*(?P<name>[a-zA-Z0-9.+\-]{2,})(\s*\(\s*(?P<relop>[>=<]+)\s*(?P<version>[0-9a-zA-Z:\-+~.]+)\s*\))?(\s*\[(?P<archs>[\s!\w\-]+)\])?\s*$')
+    __comma_sep_RE = re.compile(r'\s*,\s*')
+    __pipe_sep_RE = re.compile(r'\s*\|\s*')
+    __blank_sep_RE = re.compile(r'\s*')
+
+    @classmethod
+    def parse_relations(cls, raw):
+        """Parse a package relationship string (i.e. the value of a field like
+        Depends, Recommends, Build-Depends ...)
+        """
+        def parse_archs(raw):
+            # assumption: no space beween '!' and architecture name
+            archs = []
+            for arch in cls.__blank_sep_RE.split(raw.strip()):
+                if len(arch) and arch[0] == '!':
+                    archs.append((False, arch[1:]))
+                else:
+                    archs.append((True, arch))
+            return archs
+
+        def parse_rel(raw):
+            match = cls.__dep_RE.match(raw)
+            if match:
+                parts = match.groupdict()
+                d = { 'name': parts['name'] }
+                if not (parts['relop'] is None or parts['version'] is None):
+                    d['version'] = (parts['relop'], parts['version'])
+                else:
+                    d['version'] = None
+                if parts['archs'] is None:
+                    d['arch'] = None
+                else:
+                    d['arch'] = parse_archs(parts['archs'])
+                return d
+            else:
+                print >> sys.stderr, \
+                        'deb822.py: WARNING: cannot parse package' \
+                        ' relationship "%s", returning it raw' % raw
+                return { 'name': raw, 'version': None, 'arch': None }
+
+        tl_deps = cls.__comma_sep_RE.split(raw.strip()) # top-level deps
+        cnf = map(cls.__pipe_sep_RE.split, tl_deps)
+        return map(lambda or_deps: map(parse_rel, or_deps), cnf)
+
+    @staticmethod
+    def str(rels):
+        """Format to string structured inter-package relationships
+        
+        Perform the inverse operation of parse_relations, returning a string
+        suitable to be written in a package stanza.
+        """
+        def pp_arch(arch_spec):
+            (excl, arch) = arch_spec
+            if excl:
+                return arch
+            else:
+                return '!' + arch
+
+        def pp_atomic_dep(dep):
+            s = dep['name']
+            if dep.has_key('version') and dep['version'] is not None:
+                s += ' (%s %s)' % dep['version']
+            if dep.has_key('arch') and dep['arch'] is not None:
+                s += ' [%s]' % string.join(map(pp_arch, dep['arch']))
+            return s
+
+        pp_or_dep = lambda deps: string.join(map(pp_atomic_dep, deps), ' | ')
+        return string.join(map(pp_or_dep, rels), ', ')
+
+
+class _lowercase_dict(dict):
+    """Dictionary wrapper which lowercase keys upon lookup."""
+
+    def __getitem__(self, key):
+        return dict.__getitem__(self, key.lower())
+
+
+class _PkgRelationMixin(object):
+    """Package relationship mixin
+
+    Inheriting from this mixin you can extend a Deb882 object with attributes
+    letting you access inter-package relationship in a structured way, rather
+    than as strings. For example, while you can usually use pkg['depends'] to
+    obtain the Depends string of package pkg, mixing in with this class you
+    gain pkg.depends to access Depends as a Pkgrel instance
+
+    To use, subclass _PkgRelationMixin from a class with a _relationship_fields
+    attribute. It should be a list of field names for which structured access
+    is desired; for each of them a method wild be added to the inherited class.
+    The method name will be the lowercase version of field name; '-' will be
+    mangled as '_'. The method would return relationships in the same format of
+    the PkgRelation' relations property.
+
+    See Packages and Sources as examples.
+    """
+
+    def __init__(self, *args, **kwargs):
+        self.__relations = _lowercase_dict({})
+        self.__parsed_relations = False
+        for name in self._relationship_fields:
+            # To avoid reimplementing Deb822 key lookup logic we use a really
+            # simple dict subclass which just lowercase keys upon lookup. Since
+            # dictionary building happens only here, we ensure that all keys
+            # are in fact lowercase.
+            # With this trick we enable users to use the same key (i.e. field
+            # name) of Deb822 objects on the dictionary returned by the
+            # relations property.
+            keyname = name.lower()
+            if self.has_key(name):
+                self.__relations[keyname] = None   # lazy value
+                    # all lazy values will be expanded before setting
+                    # __parsed_relations to True
+            else:
+                self.__relations[keyname] = []
+
+    @property
+    def relations(self):
+        """Return a dictionary of inter-package relationships among the current
+        and other packages.
+
+        Dictionary keys depend on the package kind. Binary packages have keys
+        like 'depends', 'recommends', ... while source packages have keys like
+        'build-depends', 'build-depends-indep' and so on. See the Debian policy
+        for the comprehensive field list.
+
+        Dictionary values are package relationships returned as lists of lists
+        of dictionaries (see below for some examples).
+
+        The encoding of package relationships is as follows:
+        - the top-level lists corresponds to the comma-separated list of
+          Deb822, their components form a conjuction, i.e. they have to be
+          AND-ed together
+        - the inner lists corresponds to the pipe-separated list of Deb822,
+          their components form a disjunction, i.e. they have to be OR-ed
+          together
+        - member of the inner lists are dictionaries with the following keys:
+          - name:       package (or virtual package) name
+          - version:    A pair <operator, version> if the relationship is
+                        versioned, None otherwise. operator is one of "<<",
+                        "<=", "=", ">=", ">>"; version is the given version as
+                        a string.
+          - arch:       A list of pairs <polarity, architecture> if the
+                        relationship is architecture specific, None otherwise.
+                        Polarity is a boolean (false if the architecture is
+                        negated with "!", true otherwise), architecture the
+                        Debian archtiecture name as a string.
+
+        Examples:
+
+          "emacs | emacsen, make, debianutils (>= 1.7)"     becomes
+          [ [ {'name': 'emacs'}, {'name': 'emacsen'} ],
+            [ {'name': 'make'} ],
+            [ {'name': 'debianutils', 'version': ('>=', '1.7')} ] ]
+
+          "tcl8.4-dev, procps [!hurd-i386]"                 becomes
+          [ [ {'name': 'tcl8.4-dev'} ],
+            [ {'name': 'procps', 'arch': (false, 'hurd-i386')} ] ]
+        """
+        if not self.__parsed_relations:
+            lazy_rels = filter(lambda n: self.__relations[n] is None,
+                    self.__relations.keys())
+            for n in lazy_rels:
+                self.__relations[n] = PkgRelation.parse_relations(self[n])
+            self.__parsed_relations = True
+        return self.__relations
+
+class _multivalued(Deb822):
+    """A class with (R/W) support for multivalued fields.
+
+    To use, create a subclass with a _multivalued_fields attribute.  It should
+    be a dictionary with *lower-case* keys, with lists of human-readable
+    identifiers of the fields as the values.  Please see Dsc, Changes, and
+    PdiffIndex as examples.
+    """
+
+    def __init__(self, *args, **kwargs):
+        Deb822.__init__(self, *args, **kwargs)
+
+        for field, fields in self._multivalued_fields.items():
+            try:
+                contents = self[field]
+            except KeyError:
+                continue
+
+            if self.is_multi_line(contents):
+                self[field] = []
+                updater_method = self[field].append
+            else:
+                self[field] = Deb822Dict()
+                updater_method = self[field].update
+
+            for line in filter(None, contents.splitlines()):
+                updater_method(Deb822Dict(zip(fields, line.split())))
+
+    def get_as_string(self, key):
+        keyl = key.lower()
+        if keyl in self._multivalued_fields:
+            fd = StringIO.StringIO()
+            if hasattr(self[key], 'keys'): # single-line
+                array = [ self[key] ]
+            else: # multi-line
+                fd.write("\n")
+                array = self[key]
+
+            order = self._multivalued_fields[keyl]
+            try:
+                field_lengths = self._fixed_field_lengths
+            except AttributeError:
+                field_lengths = {}
+            for item in array:
+                for x in order:
+                    raw_value = str(item[x])
+                    try:
+                        length = field_lengths[keyl][x]
+                    except KeyError:
+                        value = raw_value
+                    else:
+                        value = (length - len(raw_value)) * " " + raw_value
+                    fd.write(" %s" % value)
+                fd.write("\n")
+            return fd.getvalue().rstrip("\n")
+        else:
+            return Deb822.get_as_string(self, key)
+
+###
+
+
+class _gpg_multivalued(_multivalued):
+    """A _multivalued class that can support gpg signed objects
+
+    This class's feature is that it stores the raw text before parsing so that
+    gpg can verify the signature.  Use it just like you would use the
+    _multivalued class.
+
+    This class only stores raw text if it is given a raw string, or if it
+    detects a gpg signature when given a file or sequence of lines (see
+    Deb822.split_gpg_and_payload for details).
+    """
+
+    def __init__(self, *args, **kwargs):
+        try:
+            sequence = args[0]
+        except IndexError:
+            sequence = kwargs.get("sequence", None)
+
+        if sequence is not None:
+            if isinstance(sequence, basestring):
+                self.raw_text = sequence
+            elif hasattr(sequence, "items"):
+                # sequence is actually a dict(-like) object, so we don't have
+                # the raw text.
+                pass
+            else:
+                try:
+                    gpg_pre_lines, lines, gpg_post_lines = \
+                            self.split_gpg_and_payload(sequence)
+                except EOFError:
+                    # Empty input
+                    gpg_pre_lines = lines = gpg_post_lines = []
+                if gpg_pre_lines and gpg_post_lines:
+                    raw_text = StringIO.StringIO()
+                    raw_text.write("\n".join(gpg_pre_lines))
+                    raw_text.write("\n\n")
+                    raw_text.write("\n".join(lines))
+                    raw_text.write("\n\n")
+                    raw_text.write("\n".join(gpg_post_lines))
+                    self.raw_text = raw_text.getvalue()
+                try:
+                    args = list(args)
+                    args[0] = lines
+                except IndexError:
+                    kwargs["sequence"] = lines
+
+        _multivalued.__init__(self, *args, **kwargs)
+
+
+class Dsc(_gpg_multivalued):
+    _multivalued_fields = {
+        "files": [ "md5sum", "size", "name" ],
+        "checksums-sha1": ["sha1", "size", "name"],
+        "checksums-sha256": ["sha256", "size", "name"],
+    }
+
+
+class Changes(_gpg_multivalued):
+    _multivalued_fields = {
+        "files": [ "md5sum", "size", "section", "priority", "name" ],
+        "checksums-sha1": ["sha1", "size", "name"],
+        "checksums-sha256": ["sha256", "size", "name"],
+    }
+
+    def get_pool_path(self):
+        """Return the path in the pool where the files would be installed"""
+    
+        # This is based on the section listed for the first file.  While
+        # it is possible, I think, for a package to provide files in multiple
+        # sections, I haven't seen it in practice.  In any case, this should
+        # probably detect such a situation and complain, or return a list...
+        
+        s = self['files'][0]['section']
+
+        try:
+            section, subsection = s.split('/')
+        except ValueError:
+            # main is implicit
+            section = 'main'
+
+        if self['source'].startswith('lib'):
+            subdir = self['source'][:4]
+        else:
+            subdir = self['source'][0]
+
+        return 'pool/%s/%s/%s' % (section, subdir, self['source'])
+
+
+class PdiffIndex(_multivalued):
+    _multivalued_fields = {
+        "sha1-current": [ "SHA1", "size" ],
+        "sha1-history": [ "SHA1", "size", "date" ],
+        "sha1-patches": [ "SHA1", "size", "date" ],
+    }
+
+    @property
+    def _fixed_field_lengths(self):
+        fixed_field_lengths = {}
+        for key in self._multivalued_fields:
+            if hasattr(self[key], 'keys'):
+                # Not multi-line -- don't need to compute the field length for
+                # this one
+                continue
+            length = self._get_size_field_length(key)
+            fixed_field_lengths[key] = {"size": length}
+        return fixed_field_lengths
+
+    def _get_size_field_length(self, key):
+        lengths = [len(str(item['size'])) for item in self[key]]
+        return max(lengths)
+
+
+class Release(_multivalued):
+    """Represents a Release file
+
+    Set the size_field_behavior attribute to "dak" to make the size field
+    length only as long as the longest actual value.  The default,
+    "apt-ftparchive" makes the field 16 characters long regardless.
+    """
+    # FIXME: Add support for detecting the behavior of the input, if
+    # constructed from actual 822 text.
+
+    _multivalued_fields = {
+        "md5sum": [ "md5sum", "size", "name" ],
+        "sha1": [ "sha1", "size", "name" ],
+        "sha256": [ "sha256", "size", "name" ],
+    }
+
+    __size_field_behavior = "apt-ftparchive"
+    def set_size_field_behavior(self, value):
+        if value not in ["apt-ftparchive", "dak"]:
+            raise ValueError("size_field_behavior must be either "
+                             "'apt-ftparchive' or 'dak'")
+        else:
+            self.__size_field_behavior = value
+    size_field_behavior = property(lambda self: self.__size_field_behavior,
+                                   set_size_field_behavior)
+
+    @property
+    def _fixed_field_lengths(self):
+        fixed_field_lengths = {}
+        for key in self._multivalued_fields:
+            length = self._get_size_field_length(key)
+            fixed_field_lengths[key] = {"size": length}
+        return fixed_field_lengths
+
+    def _get_size_field_length(self, key):
+        if self.size_field_behavior == "apt-ftparchive":
+            return 16
+        elif self.size_field_behavior == "dak":
+            lengths = [len(str(item['size'])) for item in self[key]]
+            return max(lengths)
+
+
+class Sources(Dsc, _PkgRelationMixin):
+    """Represent an APT source package list"""
+
+    _relationship_fields = [ 'build-depends', 'build-depends-indep',
+            'build-conflicts', 'build-conflicts-indep', 'binary' ]
+
+    def __init__(self, *args, **kwargs):
+        Dsc.__init__(self, *args, **kwargs)
+        _PkgRelationMixin.__init__(self, *args, **kwargs)
+
+
+class Packages(Deb822, _PkgRelationMixin):
+    """Represent an APT binary package list"""
+
+    _relationship_fields = [ 'depends', 'pre-depends', 'recommends',
+            'suggests', 'breaks', 'conflicts', 'provides', 'replaces',
+            'enhances' ]
+
+    def __init__(self, *args, **kwargs):
+        Deb822.__init__(self, *args, **kwargs)
+        _PkgRelationMixin.__init__(self, *args, **kwargs)
+
+###
+
+class _CaseInsensitiveString(str):
+    """Case insensitive string.
+    """
+
+    def __new__(cls, str_):
+        s = str.__new__(cls, str_)
+        s.str_lower = str_.lower()
+        s.str_lower_hash = hash(s.str_lower)
+        return s
+
+    def __hash__(self):
+        return self.str_lower_hash
+
+    def __eq__(self, other):
+        return self.str_lower == other.lower()
+
+    def lower(self):
+        return self.str_lower
+
+_strI = _CaseInsensitiveString
diff --git a/lib/debian/debfile.py b/lib/debian/debfile.py
new file mode 100644
index 0000000..a2a62f6
--- /dev/null
+++ b/lib/debian/debfile.py
@@ -0,0 +1,282 @@
+# DebFile: a Python representation of Debian .deb binary packages.
+# Copyright (C) 2007-2008   Stefano Zacchiroli  <zack at debian.org>
+# Copyright (C) 2007        Filippo Giunchedi   <filippo at debian.org>
+#
+# 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
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import gzip
+import tarfile
+
+from arfile import ArFile, ArError
+from changelog import Changelog
+from deb822 import Deb822
+
+DATA_PART = 'data.tar'      # w/o extension
+CTRL_PART = 'control.tar'
+PART_EXTS = ['gz', 'bz2']   # possible extensions
+INFO_PART = 'debian-binary'
+MAINT_SCRIPTS = ['preinst', 'postinst', 'prerm', 'postrm', 'config']
+
+CONTROL_FILE = 'control'
+CHANGELOG_NATIVE = 'usr/share/doc/%s/changelog.gz'  # with package stem
+CHANGELOG_DEBIAN = 'usr/share/doc/%s/changelog.Debian.gz'
+MD5_FILE = 'md5sums'
+
+
+class DebError(ArError):
+    pass
+
+
+class DebPart(object):
+    """'Part' of a .deb binary package.
+    
+    A .deb package is considered as made of 2 parts: a 'data' part
+    (corresponding to the 'data.tar.gz' archive embedded in a .deb) and a
+    'control' part (the 'control.tar.gz' archive). Each of them is represented
+    by an instance of this class. Each archive should be a compressed tar
+    archive; supported compression formats are: .tar.gz, .tar.bz2 .
+
+    When referring to file members of the underlying .tar.gz archive, file
+    names can be specified in one of 3 formats "file", "./file", "/file". In
+    all cases the file is considered relative to the root of the archive. For
+    the control part the preferred mechanism is the first one (as in
+    deb.control.get_content('control') ); for the data part the preferred
+    mechanism is the third one (as in deb.data.get_file('/etc/vim/vimrc') ).
+    """
+
+    def __init__(self, member):
+        self.__member = member  # arfile.ArMember file member
+        self.__tgz = None
+
+    def tgz(self):
+        """Return a TarFile object corresponding to this part of a .deb
+        package.
+        
+        Despite the name, this method gives access to various kind of
+        compressed tar archives, not only gzipped ones.
+        """
+
+        if self.__tgz is None:
+            name = self.__member.name
+            if name.endswith('.gz'):
+                gz = gzip.GzipFile(fileobj=self.__member, mode='r')
+                self.__tgz = tarfile.TarFile(fileobj=gz, mode='r')
+            elif name.endswith('.bz2'):
+                # Tarfile's __init__ doesn't allow for r:bz2 modes, but the
+                # open() classmethod does ...
+                self.__tgz = tarfile.open(fileobj=self.__member, mode='r:bz2')
+            else:
+                raise DebError("part '%s' has unexpected extension" % name)
+        return self.__tgz
+
+    @staticmethod
+    def __normalize_member(fname):
+        """ try (not so hard) to obtain a member file name in a form relative
+        to the .tar.gz root and with no heading '.' """
+
+        if fname.startswith('./'):
+            fname = fname[2:]
+        elif fname.startswith('/'):
+            fname = fname[1:]
+        return fname
+
+    # XXX in some of the following methods, compatibility among >= 2.5 and <<
+    # 2.5 python versions had to be taken into account. TarFile << 2.5 indeed
+    # was buggied and returned member file names with an heading './' only for
+    # the *first* file member. TarFile >= 2.5 fixed this and has the heading
+    # './' for all file members.
+
+    def has_file(self, fname):
+        """Check if this part contains a given file name."""
+
+        fname = DebPart.__normalize_member(fname)
+        names = self.tgz().getnames()
+        return (('./' + fname in names) \
+                or (fname in names)) # XXX python << 2.5 TarFile compatibility
+
+    def get_file(self, fname):
+        """Return a file object corresponding to a given file name."""
+
+        fname = DebPart.__normalize_member(fname)
+        try:
+            return (self.tgz().extractfile('./' + fname))
+        except KeyError:    # XXX python << 2.5 TarFile compatibility
+            return (self.tgz().extractfile(fname))
+
+    def get_content(self, fname):
+        """Return the string content of a given file, or None (e.g. for
+        directories)."""
+
+        f = self.get_file(fname)
+        content = None
+        if f:   # can be None for non regular or link files
+            content = f.read()
+            f.close()
+        return content
+
+    # container emulation
+
+    def __iter__(self):
+        return iter(self.tgz().getnames())
+
+    def __contains__(self, fname):
+        return self.has_file(fname)
+
+    def has_key(self, fname):
+        return self.has_file(fname)
+
+    def __getitem__(self, fname):
+        return self.get_content(fname)
+
+
+class DebData(DebPart):
+
+    pass
+
+
+class DebControl(DebPart):
+
+    def scripts(self):
+        """ Return a dictionary of maintainer scripts (postinst, prerm, ...)
+        mapping script names to script text. """
+
+        scripts = {}
+        for fname in MAINT_SCRIPTS:
+            if self.has_file(fname):
+                scripts[fname] = self.get_content(fname)
+
+        return scripts
+
+    def debcontrol(self):
+        """ Return the debian/control as a Deb822 (a Debian-specific dict-like
+        class) object.
+        
+        For a string representation of debian/control try
+        .get_content('control') """
+
+        return Deb822(self.get_content(CONTROL_FILE))
+
+    def md5sums(self):
+        """ Return a dictionary mapping filenames (of the data part) to
+        md5sums. Fails if the control part does not contain a 'md5sum' file.
+
+        Keys of the returned dictionary are the left-hand side values of lines
+        in the md5sums member of control.tar.gz, usually file names relative to
+        the file system root (without heading '/' or './'). """
+
+        if not self.has_file(MD5_FILE):
+            raise DebError("'%s' file not found, can't list MD5 sums" %
+                    MD5_FILE)
+
+        md5_file = self.get_file(MD5_FILE)
+        sums = {}
+        for line in md5_file.readlines():
+            # we need to support spaces in filenames, .split() is not enough
+            md5, fname = line.rstrip('\r\n').split(None, 1)
+            sums[fname] = md5
+        md5_file.close()
+        return sums
+
+
+class DebFile(ArFile):
+    """Representation of a .deb file (a Debian binary package)
+
+    DebFile objects have the following (read-only) properties:
+        - version       debian .deb file format version (not related with the
+                        contained package version), 2.0 at the time of writing
+                        for all .deb packages in the Debian archive
+        - data          DebPart object corresponding to the data.tar.gz (or
+                        other compressed tar) archive contained in the .deb
+                        file
+        - control       DebPart object corresponding to the control.tar.gz (or
+                        other compressed tar) archive contained in the .deb
+                        file
+    """
+
+    def __init__(self, filename=None, mode='r', fileobj=None):
+        ArFile.__init__(self, filename, mode, fileobj)
+        actual_names = set(self.getnames())
+
+        def compressed_part_name(basename):
+            global PART_EXTS
+            candidates = [ '%s.%s' % (basename, ext) for ext in PART_EXTS ]
+            parts = actual_names.intersection(set(candidates))
+            if not parts:
+                raise DebError("missing required part in given .deb" \
+                        " (expected one of: %s)" % candidates)
+            elif len(parts) > 1:
+                raise DebError("too many parts in given .deb" \
+                        " (was looking for only one of: %s)" % candidates)
+            else:   # singleton list
+                return list(parts)[0]
+
+        if not INFO_PART in actual_names:
+            raise DebError("missing required part in given .deb" \
+                    " (expected: '%s')" % INFO_PART)
+
+        self.__parts = {}
+        self.__parts[CTRL_PART] = DebControl(self.getmember(
+                compressed_part_name(CTRL_PART)))
+        self.__parts[DATA_PART] = DebData(self.getmember(
+                compressed_part_name(DATA_PART)))
+        self.__pkgname = None   # updated lazily by __updatePkgName
+
+        f = self.getmember(INFO_PART)
+        self.__version = f.read().strip()
+        f.close()
+
+    def __updatePkgName(self):
+        self.__pkgname = self.debcontrol()['package']
+
+    version = property(lambda self: self.__version)
+    data = property(lambda self: self.__parts[DATA_PART])
+    control = property(lambda self: self.__parts[CTRL_PART])
+
+    # proxy methods for the appropriate parts
+
+    def debcontrol(self):
+        """ See .control.debcontrol() """
+        return self.control.debcontrol()
+
+    def scripts(self):
+        """ See .control.scripts() """
+        return self.control.scripts()
+
+    def md5sums(self):
+        """ See .control.md5sums() """
+        return self.control.md5sums()
+
+    def changelog(self):
+        """ Return a Changelog object for the changelog.Debian.gz of the
+        present .deb package. Return None if no changelog can be found. """
+
+        if self.__pkgname is None:
+            self.__updatePkgName()
+
+        for fname in [ CHANGELOG_DEBIAN % self.__pkgname,
+                CHANGELOG_NATIVE % self.__pkgname ]:
+            if self.data.has_file(fname):
+                gz = gzip.GzipFile(fileobj=self.data.get_file(fname))
+                raw_changelog = gz.read()
+                gz.close()
+                return Changelog(raw_changelog)
+        return None
+
+
+if __name__ == '__main__':
+    import sys
+    deb = DebFile(filename=sys.argv[1])
+    tgz = deb.control.tgz()
+    print tgz.getmember('control')
+
diff --git a/lib/debian/debian_support.py b/lib/debian/debian_support.py
new file mode 100644
index 0000000..d216bba
--- /dev/null
+++ b/lib/debian/debian_support.py
@@ -0,0 +1,652 @@
+# debian_support.py -- Python module for Debian metadata
+# Copyright (C) 2005 Florian Weimer <fw at deneb.enyo.de>
+# Copyright (C) 2010 John Wright <jsw at debian.org>
+# 
+# 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
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+# 
+# This program 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 General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""This module implements facilities to deal with Debian-specific metadata."""
+
+import os
+import re
+import hashlib
+import types
+
+from deprecation import function_deprecated_by
+
+try:
+    import apt_pkg
+    apt_pkg.init()
+    __have_apt_pkg = True
+except ImportError:
+    __have_apt_pkg = False
+
+class ParseError(Exception):
+    """An exception which is used to signal a parse failure.
+
+    Attributes:
+
+    filename - name of the file
+    lineno - line number in the file
+    msg - error message
+
+    """
+    
+    def __init__(self, filename, lineno, msg):
+        assert type(lineno) == types.IntType
+        self.filename = filename
+        self.lineno = lineno
+        self.msg = msg
+
+    def __str__(self):
+        return self.msg
+
+    def __repr__(self):
+        return "ParseError(%s, %d, %s)" % (`self.filename`,
+                                           self.lineno,
+                                           `self.msg`)
+
+    def print_out(self, file):
+        """Writes a machine-parsable error message to file."""
+        file.write("%s:%d: %s\n" % (self.filename, self.lineno, self.msg))
+        file.flush()
+
+    printOut = function_deprecated_by(print_out)
+
+class BaseVersion(object):
+    """Base class for classes representing Debian versions
+
+    It doesn't implement any comparison, but it does check for valid versions
+    according to Section 5.6.12 in the Debian Policy Manual.  Since splitting
+    the version into epoch, upstream_version, and debian_revision components is
+    pretty much free with the validation, it sets those fields as properties of
+    the object, and sets the raw version to the full_version property.  A
+    missing epoch or debian_revision results in the respective property set to
+    None.  Setting any of the properties results in the full_version being
+    recomputed and the rest of the properties set from that.
+
+    It also implements __str__, just returning the raw version given to the
+    initializer.
+    """
+
+    re_valid_version = re.compile(
+            r"^((?P<epoch>\d+):)?"
+             "(?P<upstream_version>[A-Za-z0-9.+:~-]+?)"
+             "(-(?P<debian_revision>[A-Za-z0-9+.~]+))?$")
+    magic_attrs = ('full_version', 'epoch', 'upstream_version',
+                   'debian_revision', 'debian_version')
+
+    def __init__(self, version):
+        self.full_version = version
+
+    def _set_full_version(self, version):
+        m = self.re_valid_version.match(version)
+        if not m:
+            raise ValueError("Invalid version string %r" % version)
+
+        self.__full_version = version
+        self.__epoch = m.group("epoch")
+        self.__upstream_version = m.group("upstream_version")
+        self.__debian_revision = m.group("debian_revision")
+
+    def __setattr__(self, attr, value):
+        if attr not in self.magic_attrs:
+            super(BaseVersion, self).__setattr__(attr, value)
+            return
+
+        # For compatibility with the old changelog.Version class
+        if attr == "debian_version":
+            attr = "debian_revision"
+
+        if attr == "full_version":
+            self._set_full_version(str(value))
+        else:
+            if value is not None:
+                value = str(value)
+            private = "_BaseVersion__%s" % attr
+            old_value = getattr(self, private)
+            setattr(self, private, value)
+            try:
+                self._update_full_version()
+            except ValueError:
+                # Don't leave it in an invalid state
+                setattr(self, private, old_value)
+                self._update_full_version()
+                raise ValueError("Setting %s to %r results in invalid version"
+                                 % (attr, value))
+
+    def __getattr__(self, attr):
+        if attr not in self.magic_attrs:
+            return super(BaseVersion, self).__getattribute__(attr)
+
+        # For compatibility with the old changelog.Version class
+        if attr == "debian_version":
+            attr = "debian_revision"
+
+        private = "_BaseVersion__%s" % attr
+        return getattr(self, private)
+
+    def _update_full_version(self):
+        version = ""
+        if self.__epoch is not None:
+            version += self.__epoch + ":"
+        version += self.__upstream_version
+        if self.__debian_revision:
+            version += "-" + self.__debian_revision
+        self.full_version = version
+
+    def __str__(self):
+        return self.full_version
+
+    def __repr__(self):
+        return "%s('%s')" % (self.__class__.__name__, self)
+
+    def __cmp__(self, other):
+        raise NotImplementedError
+
+    def __hash__(self):
+        return hash(str(self))
+
+class AptPkgVersion(BaseVersion):
+    """Represents a Debian package version, using apt_pkg.VersionCompare"""
+
+    def __cmp__(self, other):
+        return apt_pkg.VersionCompare(str(self), str(other))
+
+# NativeVersion based on the DpkgVersion class by Raphael Hertzog in
+# svn://svn.debian.org/qa/trunk/pts/www/bin/common.py r2361
+class NativeVersion(BaseVersion):
+    """Represents a Debian package version, with native Python comparison"""
+
+    re_all_digits_or_not = re.compile("\d+|\D+")
+    re_digits = re.compile("\d+")
+    re_digit = re.compile("\d")
+    re_alpha = re.compile("[A-Za-z]")
+
+    def __cmp__(self, other):
+        # Convert other into an instance of BaseVersion if it's not already.
+        # (All we need is epoch, upstream_version, and debian_revision
+        # attributes, which BaseVersion gives us.) Requires other's string
+        # representation to be the raw version.
+        if not isinstance(other, BaseVersion):
+            try:
+                other = BaseVersion(str(other))
+            except ValueError, e:
+                raise ValueError("Couldn't convert %r to BaseVersion: %s"
+                                 % (other, e))
+
+        res = cmp(int(self.epoch or "0"), int(other.epoch or "0"))
+        if res != 0:
+            return res
+        res = self._version_cmp_part(self.upstream_version,
+                                     other.upstream_version)
+        if res != 0:
+            return res
+        return self._version_cmp_part(self.debian_revision or "0",
+                                      other.debian_revision or "0")
+
+    @classmethod
+    def _order(cls, x):
+        """Return an integer value for character x"""
+        if x == '~':
+            return -1
+        elif cls.re_digit.match(x):
+            return int(x) + 1
+        elif cls.re_alpha.match(x):
+            return ord(x)
+        else:
+            return ord(x) + 256
+
+    @classmethod
+    def _version_cmp_string(cls, va, vb):
+        la = [cls._order(x) for x in va]
+        lb = [cls._order(x) for x in vb]
+        while la or lb:
+            a = 0
+            b = 0
+            if la:
+                a = la.pop(0)
+            if lb:
+                b = lb.pop(0)
+            res = cmp(a, b)
+            if res != 0:
+                return res
+        return 0
+
+    @classmethod
+    def _version_cmp_part(cls, va, vb):
+        la = cls.re_all_digits_or_not.findall(va)
+        lb = cls.re_all_digits_or_not.findall(vb)
+        while la or lb:
+            a = "0"
+            b = "0"
+            if la:
+                a = la.pop(0)
+            if lb:
+                b = lb.pop(0)
+            if cls.re_digits.match(a) and cls.re_digits.match(b):
+                a = int(a)
+                b = int(b)
+                res = cmp(a, b)
+                if res != 0:
+                    return res
+            else:
+                res = cls._version_cmp_string(a, b)
+                if res != 0:
+                    return res
+        return 0
+
+if __have_apt_pkg:
+    class Version(AptPkgVersion):
+        pass
+else:
+    class Version(NativeVersion):
+        pass
+
+def version_compare(a, b):
+    return cmp(Version(a), Version(b))
+
+class PackageFile:
+    """A Debian package file.
+
+    Objects of this class can be used to read Debian's Source and
+    Packages files."""
+
+    re_field = re.compile(r'^([A-Za-z][A-Za-z0-9-]+):(?:\s*(.*?))?\s*$')
+    re_continuation = re.compile(r'^\s+(?:\.|(\S.*?)\s*)$')
+
+    def __init__(self, name, file_obj=None):
+        """Creates a new package file object.
+
+        name - the name of the file the data comes from
+        file_obj - an alternate data source; the default is to open the
+                  file with the indicated name.
+        """
+        if file_obj is None:
+            file_obj = file(name)
+        self.name = name
+        self.file = file_obj
+        self.lineno = 0
+
+    def __iter__(self):
+        line = self.file.readline()
+        self.lineno += 1
+        pkg = []
+        while line:
+            if line.strip(' \t') == '\n':
+                if len(pkg) == 0:
+                    self.raise_syntax_error('expected package record')
+                yield pkg
+                pkg = []
+                line = self.file.readline()
+                self.lineno += 1
+                continue
+            
+            match = self.re_field.match(line)
+            if not match:
+                self.raise_syntax_error("expected package field")
+            (name, contents) = match.groups()
+            contents = contents or ''
+
+            while True:
+                line = self.file.readline()
+                self.lineno += 1
+                match = self.re_continuation.match(line)
+                if match:
+                    (ncontents,) = match.groups()
+                    if ncontents is None:
+                        ncontents = ""
+                    contents = "%s\n%s" % (contents, ncontents)
+                else:
+                    break
+            pkg.append((name, contents))
+        if pkg:
+            yield pkg
+
+    def raise_syntax_error(self, msg, lineno=None):
+        if lineno is None:
+            lineno = self.lineno
+        raise ParseError(self.name, lineno, msg)
+
+    raiseSyntaxError = function_deprecated_by(raise_syntax_error)
+
+class PseudoEnum:
+    """A base class for types which resemble enumeration types."""
+    def __init__(self, name, order):
+        self._name = name
+        self._order = order
+    def __repr__(self):
+        return '%s(%s)'% (self.__class__._name__, `name`)
+    def __str__(self):
+        return self._name
+    def __cmp__(self, other):
+        return cmp(self._order, other._order)
+    def __hash__(self):
+        return hash(self._order)
+
+class Release(PseudoEnum): pass
+
+def list_releases():
+    releases = {}
+    rels = ("potato", "woody", "sarge", "etch", "lenny", "sid")
+    for r in range(len(rels)):
+        releases[rels[r]] = Release(rels[r], r)
+    Release.releases = releases
+    return releases
+
+listReleases = function_deprecated_by(list_releases)
+
+def intern_release(name, releases=list_releases()):
+    if releases.has_key(name):
+        return releases[name]
+    else:
+        return None
+
+internRelease = function_deprecated_by(intern_release)
+
+del listReleases
+del list_releases
+
+def read_lines_sha1(lines):
+    m = hashlib.sha1()
+    for l in lines:
+        m.update(l)
+    return m.hexdigest()
+
+readLinesSHA1 = function_deprecated_by(read_lines_sha1)
+
+def patches_from_ed_script(source,
+                        re_cmd=re.compile(r'^(\d+)(?:,(\d+))?([acd])$')):
+    """Converts source to a stream of patches.
+
+    Patches are triples of line indexes:
+
+    - number of the first line to be replaced
+    - one plus the number of the last line to be replaced
+    - list of line replacements
+
+    This is enough to model arbitrary additions, deletions and
+    replacements.
+    """
+
+    i = iter(source)
+    
+    for line in i:
+        match = re_cmd.match(line)
+        if match is None:
+            raise ValueError, "invalid patch command: " + `line`
+
+        (first, last, cmd) = match.groups()
+        first = int(first)
+        if last is not None:
+            last = int(last)
+
+        if cmd == 'd':
+            first = first - 1
+            if last is None:
+                last = first + 1
+            yield (first, last, [])
+            continue
+
+        if cmd == 'a':
+            if last is not None:
+                raise ValueError, "invalid patch argument: " + `line`
+            last = first
+        else:                           # cmd == c
+            first = first - 1
+            if last is None:
+                last = first + 1
+
+        lines = []
+        for l in i:
+            if l == '':
+                raise ValueError, "end of stream in command: " + `line`
+            if l == '.\n' or l == '.':
+                break
+            lines.append(l)
+        yield (first, last, lines)
+
+patchesFromEdScript = function_deprecated_by(patches_from_ed_script)
+
+def patch_lines(lines, patches):
+    """Applies patches to lines.  Updates lines in place."""
+    for (first, last, args) in patches:
+        lines[first:last] = args
+
+patchLines = function_deprecated_by(patch_lines)
+
+def replace_file(lines, local):
+
+    import os.path
+
+    local_new = local + '.new'
+    new_file = file(local_new, 'w+')
+
+    try:
+        for l in lines:
+            new_file.write(l)
+        new_file.close()
+        os.rename(local_new, local)
+    finally:
+        if os.path.exists(local_new):
+            os.unlink(local_new)
+
+replaceFile = function_deprecated_by(replace_file)
+
+def download_gunzip_lines(remote):
+    """Downloads a file from a remote location and gunzips it.
+
+    Returns the lines in the file."""
+
+    # The implementation is rather crude, but it seems that the gzip
+    # module needs a real file for input.
+
+    import gzip
+    import tempfile
+    import urllib
+
+    (handle, fname) = tempfile.mkstemp()
+    try:
+        os.close(handle)
+        (filename, headers) = urllib.urlretrieve(remote, fname)
+        gfile = gzip.GzipFile(filename)
+        lines = gfile.readlines()
+        gfile.close()
+    finally:
+        os.unlink(fname)
+    return lines
+
+downloadGunzipLines = function_deprecated_by(download_gunzip_lines)
+
+def download_file(remote, local):
+    """Copies a gzipped remote file to the local system.
+
+    remote - URL, without the .gz suffix
+    local - name of the local file
+    """
+    
+    lines = download_gunzip_lines(remote + '.gz')
+    replace_file(lines, local)
+    return lines
+
+downloadFile = function_deprecated_by(download_file)
+
+def update_file(remote, local, verbose=None):
+    """Updates the local file by downloading a remote patch.
+
+    Returns a list of lines in the local file.
+    """
+
+    try:
+        local_file = file(local)
+    except IOError:
+        if verbose:
+            print "update_file: no local copy, downloading full file"
+        return download_file(remote, local)
+
+    lines = local_file.readlines()
+    local_file.close()
+    local_hash = read_lines_sha1(lines)
+    patches_to_apply = []
+    patch_hashes = {}
+    
+    import urllib
+    index_name = remote + '.diff/Index'
+
+    re_whitespace=re.compile('\s+')
+
+    try:
+        index_url = urllib.urlopen(index_name)
+        index_fields = list(PackageFile(index_name, index_url))
+    except ParseError:
+        # FIXME: urllib does not raise a proper exception, so we parse
+        # the error message.
+        if verbose:
+            print "update_file: could not interpret patch index file"
+        return download_file(remote, local)
+    except IOError:
+        if verbose:
+            print "update_file: could not download patch index file"
+        return download_file(remote, local)
+
+    for fields in index_fields:
+        for (field, value) in fields:
+            if field == 'SHA1-Current':
+                (remote_hash, remote_size) = re_whitespace.split(value)
+                if local_hash == remote_hash:
+                    if verbose:
+                        print "update_file: local file is up-to-date"
+                    return lines
+                continue
+
+            if field =='SHA1-History':
+                for entry in value.splitlines():
+                    if entry == '':
+                        continue
+                    (hist_hash, hist_size, patch_name) \
+                                = re_whitespace.split(entry)
+
+                    # After the first patch, we have to apply all
+                    # remaining patches.
+                    if patches_to_apply or  hist_hash == local_hash:
+                        patches_to_apply.append(patch_name)
+                        
+                continue
+            
+            if field == 'SHA1-Patches':
+                for entry in value.splitlines():
+                    if entry == '':
+                        continue
+                    (patch_hash, patch_size, patch_name) \
+                                 = re_whitespace.split(entry)
+                    patch_hashes[patch_name] = patch_hash
+                continue
+            
+            if verbose:
+                print "update_file: field %s ignored" % `field`
+        
+    if not patches_to_apply:
+        if verbose:
+            print "update_file: could not find historic entry", local_hash
+        return download_file(remote, local)
+
+    for patch_name in patches_to_apply:
+        print "update_file: downloading patch " + `patch_name`
+        patch_contents = download_gunzip_lines(remote + '.diff/' + patch_name
+                                          + '.gz')
+        if read_lines_sha1(patch_contents ) <> patch_hashes[patch_name]:
+            raise ValueError, "patch %s was garbled" % `patch_name`
+        patch_lines(lines, patches_from_ed_script(patch_contents))
+        
+    new_hash = read_lines_sha1(lines)
+    if new_hash <> remote_hash:
+        raise ValueError, ("patch failed, got %s instead of %s"
+                           % (new_hash, remote_hash))
+
+    replace_file(lines, local)
+    return lines
+
+updateFile = function_deprecated_by(update_file)
+
+def merge_as_sets(*args):
+    """Create an order set (represented as a list) of the objects in
+    the sequences passed as arguments."""
+    s = {}
+    for x in args:
+        for y in x:
+            s[y] = True
+    l = s.keys()
+    l.sort()
+    return l
+
+mergeAsSets = function_deprecated_by(merge_as_sets)
+
+def test():
+    # Version
+    for (cls1, cls2) in [(AptPkgVersion, AptPkgVersion),
+                         (AptPkgVersion, NativeVersion),
+                         (NativeVersion, AptPkgVersion),
+                         (NativeVersion, NativeVersion),
+                         (str, AptPkgVersion), (AptPkgVersion, str),
+                         (str, NativeVersion), (NativeVersion, str)]:
+        assert cls1('0') < cls2('a')
+        assert cls1('1.0') < cls2('1.1')
+        assert cls1('1.2') < cls2('1.11')
+        assert cls1('1.0-0.1') < cls2('1.1')
+        assert cls1('1.0-0.1') < cls2('1.0-1')
+        assert cls1('1.0') == cls2('1.0')
+        assert cls1('1.0-0.1') == cls2('1.0-0.1')
+        assert cls1('1:1.0-0.1') == cls2('1:1.0-0.1')
+        assert cls1('1:1.0') == cls2('1:1.0')
+        assert cls1('1.0-0.1') < cls2('1.0-1')
+        assert cls1('1.0final-5sarge1') > cls2('1.0final-5') \
+               > cls2('1.0a7-2')
+        assert cls1('0.9.2-5') < cls2('0.9.2+cvs.1.0.dev.2004.07.28-1.5')
+        assert cls1('1:500') < cls2('1:5000')
+        assert cls1('100:500') > cls2('11:5000')
+        assert cls1('1.0.4-2') > cls2('1.0pre7-2')
+        assert cls1('1.5~rc1') < cls2('1.5')
+        assert cls1('1.5~rc1') < cls2('1.5+b1')
+        assert cls1('1.5~rc1') < cls2('1.5~rc2')
+        assert cls1('1.5~rc1') > cls2('1.5~dev0')
+
+    # Release
+    assert intern_release('sarge') < intern_release('etch')
+
+    # PackageFile
+    # for p in PackageFile('../../data/packages/sarge/Sources'):
+    #     assert p[0][0] == 'Package'
+    # for p in PackageFile('../../data/packages/sarge/Packages.i386'):
+    #     assert p[0][0] == 'Package'
+
+    # Helper routines
+    assert read_lines_sha1([]) == 'da39a3ee5e6b4b0d3255bfef95601890afd80709'
+    assert read_lines_sha1(['1\n', '23\n']) \
+           == '14293c9bd646a15dc656eaf8fba95124020dfada'
+
+    file_a = map(lambda x: "%d\n" % x, range(1, 18))
+    file_b = ['0\n', '1\n', '<2>\n', '<3>\n', '4\n', '5\n', '7\n', '8\n',
+              '11\n', '12\n', '<13>\n', '14\n', '15\n', 'A\n', 'B\n', 'C\n',
+              '16\n', '17\n',]
+    patch = ['15a\n', 'A\n', 'B\n', 'C\n', '.\n', '13c\n', '<13>\n', '.\n',
+             '9,10d\n', '6d\n', '2,3c\n', '<2>\n', '<3>\n', '.\n', '0a\n',
+             '0\n', '.\n']
+    patch_lines(file_a, patches_from_ed_script(patch))
+    assert ''.join(file_b) == ''.join(file_a)
+
+    assert len(merge_as_sets([])) == 0
+    assert ''.join(merge_as_sets("abc", "cb")) == "abc"
+
+if __name__ == "__main__":
+    test()
diff --git a/lib/debian/debtags.py b/lib/debian/debtags.py
new file mode 100644
index 0000000..cc44f14
--- /dev/null
+++ b/lib/debian/debtags.py
@@ -0,0 +1,505 @@
+
+# debtags.py -- Access and manipulate Debtags information
+# Copyright (C) 2006-2007  Enrico Zini <enrico at enricozini.org>
+#
+# 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
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import re, cPickle
+
+from deprecation import function_deprecated_by
+
+def parse_tags(input):
+	lre = re.compile(r"^(.+?)(?::?\s*|:\s+(.+?)\s*)$")
+	for line in input:
+		# Is there a way to remove the last character of a line that does not
+		# make a copy of the entire line?
+		m = lre.match(line)
+		pkgs = set(m.group(1).split(', '))
+		if m.group(2):
+			tags = set(m.group(2).split(', '))
+		else:
+			tags = set()
+		yield pkgs, tags
+
+parseTags = function_deprecated_by(parse_tags)
+
+def read_tag_database(input):
+	"Read the tag database, returning a pkg->tags dictionary"
+	db = {}
+	for pkgs, tags in parse_tags(input):
+		# Create the tag set using the native set
+		for p in pkgs:
+			db[p] = tags.copy()
+	return db;
+
+readTagDatabase = function_deprecated_by(read_tag_database)
+
+def read_tag_database_reversed(input):
+	"Read the tag database, returning a tag->pkgs dictionary"
+	db = {}
+	for pkgs, tags in parse_tags(input):
+		# Create the tag set using the native set
+		for tag in tags:
+			if db.has_key(tag):
+				db[tag] |= pkgs
+			else:
+				db[tag] = pkgs.copy()
+	return db;
+
+readTagDatabaseReversed = function_deprecated_by(read_tag_database_reversed)
+
+def read_tag_database_both_ways(input, tag_filter = None):
+	"Read the tag database, returning a pkg->tags and a tag->pkgs dictionary"
+	db = {}
+	dbr = {}
+	for pkgs, tags in parse_tags(input):
+		# Create the tag set using the native set
+		if tag_filter == None:
+			tags = set(tags)
+		else:
+			tags = set(filter(tag_filter, tags))
+		for pkg in pkgs:
+			db[pkg] = tags.copy()
+		for tag in tags:
+			if dbr.has_key(tag):
+				dbr[tag] |= pkgs
+			else:
+				dbr[tag] = pkgs.copy()
+	return db, dbr;
+
+readTagDatabaseBothWays = function_deprecated_by(read_tag_database_both_ways)
+
+def reverse(db):
+	"Reverse a tag database, from package -> tags to tag->packages"
+	res = {}
+	for pkg, tags in db.items():
+		for tag in tags:
+			if not res.has_key(tag):
+				res[tag] = set()
+			res[tag].add(pkg)
+	return res
+
+
+def output(db):
+	"Write the tag database"
+	for pkg, tags in db.items():
+		# Using % here seems awkward to me, but if I use calls to
+		# sys.stdout.write it becomes a bit slower
+		print "%s:" % (pkg), ", ".join(tags)
+
+
+def relevance_index_function(full, sub):
+	#return (float(sub.card(tag)) / float(sub.tag_count())) / \
+	#       (float(full.card(tag)) / float(full.tag_count()))
+	#return sub.card(tag) * full.card(tag) / sub.tag_count()
+
+	# New cardinality divided by the old cardinality
+	#return float(sub.card(tag)) / float(full.card(tag))
+
+	## Same as before, but weighted by the relevance the tag had in the
+	## full collection, to downplay the importance of rare tags
+	#return float(sub.card(tag) * full.card(tag)) / float(full.card(tag) * full.tag_count())
+	# Simplified version:
+	#return float(sub.card(tag)) / float(full.tag_count())
+	
+	# Weighted by the square root of the relevance, to downplay the very
+	# common tags a bit
+	#return lambda tag: float(sub.card(tag)) / float(full.card(tag)) * math.sqrt(full.card(tag) / float(full.tag_count()))
+	#return lambda tag: float(sub.card(tag)) / float(full.card(tag)) * math.sqrt(full.card(tag) / float(full.package_count()))
+	# One useless factor removed, and simplified further, thanks to Benjamin Mesing
+	return lambda tag: float(sub.card(tag)**2) / float(full.card(tag))
+
+	# The difference between how many packages are in and how many packages are out
+	# (problems: tags that mean many different things can be very much out
+	# as well.  In the case of 'image editor', for example, there will be
+	# lots of editors not for images in the outside group.
+	# It is very, very good for nonambiguous keywords like 'image'.
+	#return lambda tag: 2 * sub.card(tag) - full.card(tag)
+	# Same but it tries to downplay the 'how many are out' value in the
+	# case of popular tags, to mitigate the 'there will always be popular
+	# tags left out' cases.  Does not seem to be much of an improvement.
+	#return lambda tag: sub.card(tag) - float(full.card(tag) - sub.card(tag))/(math.sin(float(full.card(tag))*3.1415/full.package_count())/4 + 0.75)
+
+relevanceIndexFunction = function_deprecated_by(relevance_index_function)
+
+class DB:
+	"""
+	In-memory database mapping packages to tags and tags to packages.
+	"""
+
+	def __init__(self):
+		self.db = {}
+		self.rdb = {}
+	
+	def read(self, input, tag_filter=None):
+		"""
+		Read the database from a file.
+
+		Example::
+			# Read the system Debtags database
+			db.read(open("/var/lib/debtags/package-tags", "r"))
+		"""
+		self.db, self.rdb = read_tag_database_both_ways(input, tag_filter)
+
+	def qwrite(self, file):
+		"Quickly write the data to a pickled file"
+		cPickle.dump(self.db, file)
+		cPickle.dump(self.rdb, file)
+
+	def qread(self, file):
+		"Quickly read the data from a pickled file"
+		self.db = cPickle.load(file)
+		self.rdb = cPickle.load(file)
+
+	def insert(self, pkg, tags):
+		self.db[pkg] = tags.copy()
+		for tag in tags:
+			if self.rdb.has_key(tag):
+				self.rdb[tag].add(pkg)
+			else:
+				self.rdb[tag] = set((pkg))
+
+	def dump(self):
+		output(self.db)
+
+	def dump_reverse(self):
+		output(self.rdb)
+
+	dumpReverse = function_deprecated_by(dump_reverse)
+	
+	def reverse(self):
+		"Return the reverse collection, sharing tagsets with this one"
+		res = DB()
+		res.db = self.rdb
+		res.rdb = self.db
+		return res
+
+	def facet_collection(self):
+		"""
+		Return a copy of this collection, but replaces the tag names
+		with only their facets.
+		"""
+		fcoll = DB()
+		tofacet = re.compile(r"^([^:]+).+")
+		for pkg, tags in self.iter_packagesTags():
+			ftags = set([tofacet.sub(r"\1", t) for t in tags])
+			fcoll.insert(pkg, ftags)
+		return fcoll
+
+	facetCollection = function_deprecated_by(facet_collection)
+
+	def copy(self):
+		"""
+		Return a copy of this collection, with the tagsets copied as
+		well.
+		"""
+		res = DB()
+		res.db = self.db.copy()
+		res.rdb = self.rdb.copy()
+		return res
+
+	def reverse_copy(self):
+		"""
+		Return the reverse collection, with a copy of the tagsets of
+		this one.
+		"""
+		res = DB()
+		res.db = self.rdb.copy()
+		res.rdb = self.db.copy()
+		return res
+
+	reverseCopy = function_deprecated_by(reverse_copy)
+
+	def choose_packages(self, package_iter):
+		"""
+		Return a collection with only the packages in package_iter,
+		sharing tagsets with this one
+		"""
+		res = DB()
+		db = {}
+		for pkg in package_iter:
+			if self.db.has_key(pkg): db[pkg] = self.db[pkg]
+		res.db = db
+		res.rdb = reverse(db)
+		return res
+
+	choosePackages = function_deprecated_by(choose_packages)
+
+	def choose_packages_copy(self, package_iter):
+		"""
+		Return a collection with only the packages in package_iter,
+		with a copy of the tagsets of this one
+		"""
+		res = DB()
+		db = {}
+		for pkg in package_iter:
+			db[pkg] = self.db[pkg]
+		res.db = db
+		res.rdb = reverse(db)
+		return res
+
+	choosePackagesCopy = function_deprecated_by(choose_packages_copy)
+
+	def filter_packages(self, package_filter):
+		"""
+		Return a collection with only those packages that match a
+		filter, sharing tagsets with this one.  The filter will match
+		on the package.
+		"""
+		res = DB()
+		db = {}
+		for pkg in filter(package_filter, self.db.iterkeys()):
+			db[pkg] = self.db[pkg]
+		res.db = db
+		res.rdb = reverse(db)
+		return res
+
+	filterPackages = function_deprecated_by(filter_packages)
+
+	def filter_packages_copy(self, filter):
+		"""
+		Return a collection with only those packages that match a
+		filter, with a copy of the tagsets of this one.  The filter
+		will match on the package.
+		"""
+		res = DB()
+		db = {}
+		for pkg in filter(filter, self.db.iterkeys()):
+			db[pkg] = self.db[pkg].copy()
+		res.db = db
+		res.rdb = reverse(db)
+		return res
+
+	filterPackagesCopy = function_deprecated_by(filter_packages_copy)
+
+	def filter_packages_tags(self, package_tag_filter):
+		"""
+		Return a collection with only those packages that match a
+		filter, sharing tagsets with this one.  The filter will match
+		on (package, tags).
+		"""
+		res = DB()
+		db = {}
+		for pkg, tags in filter(package_tag_filter, self.db.iteritems()):
+			db[pkg] = self.db[pkg]
+		res.db = db
+		res.rdb = reverse(db)
+		return res
+
+	filterPackagesTags = function_deprecated_by(filter_packages_tags)
+
+	def filter_packages_tags_copy(self, package_tag_filter):
+		"""
+		Return a collection with only those packages that match a
+		filter, with a copy of the tagsets of this one.  The filter
+		will match on (package, tags).
+		"""
+		res = DB()
+		db = {}
+		for pkg, tags in filter(package_tag_filter, self.db.iteritems()):
+			db[pkg] = self.db[pkg].copy()
+		res.db = db
+		res.rdb = reverse(db)
+		return res
+
+	filterPackagesTagsCopy = function_deprecated_by(filter_packages_tags_copy)
+
+	def filter_tags(self, tag_filter):
+		"""
+		Return a collection with only those tags that match a
+		filter, sharing package sets with this one.  The filter will match
+		on the tag.
+		"""
+		res = DB()
+		rdb = {}
+		for tag in filter(tag_filter, self.rdb.iterkeys()):
+			rdb[tag] = self.rdb[tag]
+		res.rdb = rdb
+		res.db = reverse(rdb)
+		return res
+
+	filterTags = function_deprecated_by(filter_tags)
+
+	def filter_tags_copy(self, tag_filter):
+		"""
+		Return a collection with only those tags that match a
+		filter, with a copy of the package sets of this one.  The
+		filter will match on the tag.
+		"""
+		res = DB()
+		rdb = {}
+		for tag in filter(tag_filter, self.rdb.iterkeys()):
+			rdb[tag] = self.rdb[tag].copy()
+		res.rdb = rdb
+		res.db = reverse(rdb)
+		return res
+
+	filterTagsCopy = function_deprecated_by(filter_tags_copy)
+
+	def has_package(self, pkg):
+		"""Check if the collection contains the given package"""
+		return self.db.has_key(pkg)
+
+	hasPackage = function_deprecated_by(has_package)
+
+	def has_tag(self, tag):
+		"""Check if the collection contains packages tagged with tag"""
+		return self.rdb.has_key(tag)
+
+	hasTag = function_deprecated_by(has_tag)
+
+	def tags_of_package(self, pkg):
+		"""Return the tag set of a package"""
+		return self.db.has_key(pkg) and self.db[pkg] or set()
+
+	tagsOfPackage = function_deprecated_by(tags_of_package)
+
+	def packages_of_tag(self, tag):
+		"""Return the package set of a tag"""
+		return self.rdb.has_key(tag) and self.rdb[tag] or set()
+
+	packagesOfTag = function_deprecated_by(packages_of_tag)
+
+	def tags_of_packages(self, pkgs):
+		"""Return the set of tags that have all the packages in pkgs"""
+		res = None
+		for p in pkgs:
+			if res == None:
+				res = set(self.tags_of_package(p))
+			else:
+				res &= self.tags_of_package(p)
+		return res
+
+	tagsOfPackages = function_deprecated_by(tags_of_packages)
+
+	def packages_of_tags(self, tags):
+		"""Return the set of packages that have all the tags in tags"""
+		res = None
+		for t in tags:
+			if res == None:
+				res = set(self.packages_of_tag(t))
+			else:
+				res &= self.packages_of_tag(t)
+		return res
+
+	packagesOfTags = function_deprecated_by(packages_of_tags)
+
+	def card(self, tag):
+		"""
+		Return the cardinality of a tag
+		"""
+		return self.rdb.has_key(tag) and len(self.rdb[tag]) or 0
+
+	def discriminance(self, tag):
+		"""
+		Return the discriminance index if the tag.
+		
+		Th discriminance index of the tag is defined as the minimum
+		number of packages that would be eliminated by selecting only
+		those tagged with this tag or only those not tagged with this
+		tag.
+		"""
+		n = self.card(tag)
+		tot = self.package_count()
+		return min(n, tot - n)
+
+	def iter_packages(self):
+		"""Iterate over the packages"""
+		return self.db.iterkeys()
+
+	iterPackages = function_deprecated_by(iter_packages)
+
+	def iter_tags(self):
+		"""Iterate over the tags"""
+		return self.rdb.iterkeys()
+
+	iterTags = function_deprecated_by(iter_tags)
+
+	def iter_packages_tags(self):
+		"""Iterate over 2-tuples of (pkg, tags)"""
+		return self.db.iteritems()
+
+	iterPackagesTags = function_deprecated_by(iter_packages_tags)
+
+	def iter_tags_packages(self):
+		"""Iterate over 2-tuples of (tag, pkgs)"""
+		return self.rdb.iteritems()
+
+	iterTagsPackages = function_deprecated_by(iter_tags_packages)
+
+	def package_count(self):
+		"""Return the number of packages"""
+		return len(self.db)
+
+	packageCount = function_deprecated_by(package_count)
+
+	def tag_count(self):
+		"""Return the number of tags"""
+		return len(self.rdb)
+
+	tagCount = function_deprecated_by(tag_count)
+
+	def ideal_tagset(self, tags):
+		"""
+		Return an ideal selection of the top tags in a list of tags.
+
+		Return the tagset made of the highest number of tags taken in
+		consecutive sequence from the beginning of the given vector,
+		that would intersecate with the tagset of a comfortable amount
+		of packages.
+
+		Comfortable is defined in terms of how far it is from 7.
+		"""
+
+		# TODO: the scoring function is quite ok, but may need more
+		# tuning.  I also center it on 15 instead of 7 since we're
+		# setting a starting point for the search, not a target point
+		def score_fun(x):
+			return float((x-15)*(x-15))/x
+
+		hits = []
+		tagset = set()
+		min_score = 3
+		for i in range(len(tags)):
+			pkgs = self.packages_of_tags(tags[:i+1])
+			card = len(pkgs)
+			if card == 0: break;
+			score = score_fun(card)
+			if score < min_score:
+				min_score = score
+				tagset = set(tags[:i+1])
+
+		# Return always at least the first tag
+		if len(tagset) == 0:
+			return set(tags[:1])
+		else:
+			return tagset
+
+	idealTagset = function_deprecated_by(ideal_tagset)
+
+	def correlations(self):
+		"""
+		Generate the list of correlation as a tuple (hastag, hasalsotag, score).
+
+		Every touple will indicate that the tag 'hastag' tends to also
+		have 'hasalsotag' with a score of 'score'.
+		"""
+		for pivot in self.iter_tags():
+			with_ = self.filter_packages_tags(lambda pt: pivot in pt[1])
+			without = self.filter_packages_tags(lambda pt: pivot not in pt[1])
+			for tag in with_.iter_tags():
+				if tag == pivot: continue
+				has = float(with_.card(tag)) / float(with_.package_count())
+				hasnt = float(without.card(tag)) / float(without.package_count())
+				yield pivot, tag, has - hasnt
diff --git a/lib/debian/deprecation.py b/lib/debian/deprecation.py
new file mode 100644
index 0000000..6ed7e5a
--- /dev/null
+++ b/lib/debian/deprecation.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*- vim: fileencoding=utf-8 :
+#
+# debian_bundle/deprecation.py
+# Utility module to deprecate features
+#
+# Copyright © Ben Finney <ben+debian at benfinney.id.au>
+#
+# 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 the Free Software Foundation, either version 2
+# of the License, or (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+""" Utility module to deprecate features """
+
+import warnings
+
+def function_deprecated_by(func):
+    """ Return a function that warns it is deprecated by another function.
+
+        Returns a new function that warns it is deprecated by function
+        ``func``, then acts as a pass-through wrapper for ``func``.
+
+        """
+    func_name = func.__name__
+    warn_msg = "Use %(func_name)s instead" % vars()
+    def deprecated_func(*args, **kwargs):
+        warnings.warn(warn_msg, DeprecationWarning, stacklevel=2)
+        return func(*args, **kwargs)
+    return deprecated_func
diff --git a/lib/debian/doc-debtags b/lib/debian/doc-debtags
new file mode 100755
index 0000000..fecc77f
--- /dev/null
+++ b/lib/debian/doc-debtags
@@ -0,0 +1,98 @@
+#!/usr/bin/python
+
+import debtags
+import sys
+import inspect
+
+def print_indented (spaces, string):
+	for line in string.split("\n"):
+		for i in range(1,spaces):
+			sys.stdout.write(" ")
+		sys.stdout.write(line)
+		sys.stdout.write("\n")
+
+def document (callable):
+	if callable.__doc__ != None:
+		print_indented(2, callable.__name__)
+		print_indented(4, inspect.getdoc(callable))
+		print
+
+
+print """debtags.py README
+=================
+
+The Debtags python module provides support for accessing and manipulating
+Debtags tag data.
+
+The module provides a single class, debtags.DB, which implements various kinds
+of tag operations on an in-memory tag database.
+
+The database can be queried both as a database of packages with associated tags
+and as a database of tags with associated packages.  Performance are good in
+both ways: querying the tags of a package has the same peed as querying the
+packages having a tag.
+
+debtags.DB allows both simple queries and more complex algorithms to be
+implemented easily and efficiently.  Have a look at the Sample usage section
+below for some examples.
+
+
+Classes
+=======
+
+There is only one class: debtags.DB:
+"""
+
+document (debtags.DB)
+
+print """
+The methods of debtags.DB are:
+"""
+
+for m in dir(debtags.DB):
+	if m[0:2] != '__' and callable(getattr(debtags.DB, m)):
+		document(getattr(debtags.DB, m))
+
+print """Iteration
+=========
+
+debtags.DB provides various iteration methods to iterate the collection either
+in a package-centered or in a tag-centered way:
+"""
+
+document(debtags.DB.iter_packages)
+document(debtags.DB.iter_packages_tags)
+document(debtags.DB.iter_tags)
+document(debtags.DB.iter_tags_packages)
+
+
+print """Sample usage
+============
+
+This example reads the system debtags database and performs a simple tag
+search::
+
+    import debtags
+    
+    db = debtags.DB()
+    db.read(open("/var/lib/debtags/package-tags", "r"))
+    print db.package_count(), "packages in the database"
+    print "Image editors:"
+    for pkg in db.packages_of_tags(set(("use::editing", "works-with::image:raster"))):
+    	print " *", pkg
+
+This example computes the set of tags that belong to all the packages in a
+list, then shows all the other packages that have those tags:
+
+    import debtags
+
+    db = debtags.DB()
+    db.read(open("/var/lib/debtags/package-tags", "r"))
+    tags = db.tags_of_packages(("gimp", "krita"))
+    print "Common tags:"
+    for tag in tags:
+	print " *", tag
+    print "Packages similar to gimp and krita:"
+    for pkg in db.packages_of_tags(tags):
+	print " *", pkg
+"""
diff --git a/lib/debian_bundle/__init__.py b/lib/debian_bundle/__init__.py
new file mode 100644
index 0000000..05b5b83
--- /dev/null
+++ b/lib/debian_bundle/__init__.py
@@ -0,0 +1,9 @@
+import os
+import sys
+
+sys.stderr.write("WARNING: the 'debian_bundle' package is *DEPRECATED*; "
+                 "use the 'debian' package\n")
+
+# Support "from debian_bundle import foo"
+parent_dir = os.path.dirname(__path__[0])
+__path__.append(os.path.join(parent_dir, "debian"))
diff --git a/setup.py.in b/setup.py.in
index 3986bf2..0bf6475 100644
--- a/setup.py.in
+++ b/setup.py.in
@@ -22,7 +22,8 @@ setup(name='python-debian',
       version='__CHANGELOG_VERSION__',
       description='Debian package related modules',
       url='http://packages.debian.org/sid/python-debian',
-      packages=['debian_bundle'],
+      package_dir={'': 'lib'},
+      packages=['debian', 'debian_bundle'],
       py_modules=['deb822'],
       maintainer='Debian python-debian Maintainers',
       maintainer_email='pkg-python-debian-maint at lists.alioth.debian.org',
diff --git a/tests/test_changelog.py b/tests/test_changelog.py
index 9c4bdd5..33c017f 100755
--- a/tests/test_changelog.py
+++ b/tests/test_changelog.py
@@ -26,7 +26,7 @@
 import sys
 import unittest
 
-sys.path.insert(0, '../debian_bundle/')
+sys.path.insert(0, '../lib/debian/')
 
 import changelog
 
diff --git a/tests/test_deb822.py b/tests/test_deb822.py
index f9a1f55..02ca3ce 100755
--- a/tests/test_deb822.py
+++ b/tests/test_deb822.py
@@ -23,7 +23,7 @@ import sys
 import unittest
 from StringIO import StringIO
 
-sys.path.insert(0, '../debian_bundle/')
+sys.path.insert(0, '../lib/debian/')
 
 import deb822
 
diff --git a/tests/test_debfile.py b/tests/test_debfile.py
index 1ac598f..2287521 100755
--- a/tests/test_debfile.py
+++ b/tests/test_debfile.py
@@ -25,7 +25,7 @@ import sys
 import tempfile
 import uu
 
-sys.path.insert(0, '../debian_bundle/')
+sys.path.insert(0, '../lib/debian/')
 
 import arfile
 import debfile
diff --git a/tests/test_debtags.py b/tests/test_debtags.py
index cc7c3d8..cbe6674 100755
--- a/tests/test_debtags.py
+++ b/tests/test_debtags.py
@@ -20,7 +20,7 @@
 import sys
 import unittest
 
-sys.path.insert(0, '../debian_bundle/')
+sys.path.insert(0, '../lib/debian/')
 import debtags
 
 class TestDebtags(unittest.TestCase):
-- 
1.6.3.3




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