Description: Port the code to work bilingually in Python 2.7, 3.4, and 3.5.
Author: Barry Warsaw <barry@debian.org>
Bug: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=804099

--- a/axi-cache
+++ b/axi-cache
@@ -25,8 +25,11 @@
 #  - save NAME  save the query to be recalled later with @NAME (or if notmuch
 #    has a syntax for saved queries, recycle it)
 
+from __future__ import print_function
+
+from operator import itemgetter
 from optparse import OptionParser
-from cStringIO import StringIO
+from six import StringIO
 import sys
 import os, os.path
 import axi
@@ -54,8 +57,8 @@
     except ImportError:
         from debian_bundle import deb822
     helponly = False
-except ImportError, e:
-    print >>sys.stderr, "%s: only help functions are implemented, for the sake of help2man" % str(e)
+except ImportError as e:
+    print("%s: only help functions are implemented, for the sake of help2man" % str(e), file=sys.stderr)
     helponly = True
 
 if not helponly:
@@ -74,9 +77,9 @@
     def readVocabulary():
         try:
             fin = open(DEBTAGS_VOCABULARY)
-        except Exception, e:
+        except Exception as e:
             # Only show this when being verbose
-            print >>sys.stderr, "Cannot read %s: %s. Please install `debtags' t" % (DEBTAGS_VOCABULARY, str(e))
+            print("Cannot read %s: %s. Please install `debtags' t" % (DEBTAGS_VOCABULARY, str(e)), file=sys.stderr)
             return None, None
         facets = dict()
         tags = dict()
@@ -142,9 +145,9 @@
             if os.path.exists(CACHEFILE):
                 try:
                     self.cache.read(CACHEFILE)
-                except Exception, e:
-                    print >>sys.stderr, e
-                    print >>sys.stderr, "ignoring %s which seems to be corrupted" % CACHEFILE
+                except Exception as e:
+                    print(e, file=sys.stderr)
+                    print("ignoring %s which seems to be corrupted" % CACHEFILE, file=sys.stderr)
 
             self.dirty = False
             self.facets = None
@@ -154,7 +157,7 @@
             "Save the state so we find it next time"
             if self.dirty:
                 if not os.path.exists(XDG_CACHE_HOME):
-                    os.makedirs(XDG_CACHE_HOME, mode=0700)
+                    os.makedirs(XDG_CACHE_HOME, mode=0o700)
                 self.cache.write(open(CACHEFILE, "w"))
                 self.dirty = False
 
@@ -400,7 +403,7 @@
         import datetime
         import axi.indexer
         # General info
-        print "Main data directory:", axi.XAPIANDBPATH
+        print("Main data directory:", axi.XAPIANDBPATH)
         try:
             cur_timestamp = os.path.getmtime(axi.XAPIANDBSTAMP)
             cur_time = time.strftime("%c", time.localtime(cur_timestamp))
@@ -408,22 +411,22 @@
         except e:
             cur_timestamp = 0
             cur_time = "not available: " + str(e)
-        print "Update timestamp: %s (%s)" % (axi.XAPIANDBSTAMP, cur_time)
+        print("Update timestamp: %s (%s)" % (axi.XAPIANDBSTAMP, cur_time))
         try:
             index_loc = open(axi.XAPIANINDEX).read().split(" ", 1)[1].strip()
             index_loc = "pointing to " + index_loc
         except e:
             index_loc = "not available: " + str(e)
-        print "Index location: %s (%s)" % (axi.XAPIANINDEX, index_loc)
+        print("Index location: %s (%s)" % (axi.XAPIANINDEX, index_loc))
         def fileinfo(fname):
             if os.path.exists(fname):
                 return fname
             else:
                 return fname + " (available from next reindex)"
-        print "Documentation of index contents:", fileinfo(axi.XAPIANDBDOC)
-        print "Documentation of available prefixes:", fileinfo(axi.XAPIANDBPREFIXES)
-        print "Documentation of available values:", fileinfo(axi.XAPIANDBVALUES)
-        print "Plugin directory:", axi.PLUGINDIR
+        print("Documentation of index contents:", fileinfo(axi.XAPIANDBDOC))
+        print("Documentation of available prefixes:", fileinfo(axi.XAPIANDBPREFIXES))
+        print("Documentation of available values:", fileinfo(axi.XAPIANDBVALUES))
+        print("Plugin directory:", axi.PLUGINDIR)
 
         # Aggregated plugin information
         # { name: { path=path, desc=desc, status=status } }
@@ -432,7 +435,7 @@
         # Aggregated value information
         # { valuename: { val=num, desc=shortdesc, plugins=[plugin names] } }
         values, descs = axi.readValueDB()
-        values = dict(((a, dict(val=b, desc=descs[a], plugins=[])) for a, b in values.iteritems()))
+        values = dict(((a, dict(val=b, desc=descs[a], plugins=[])) for a, b in values.items()))
 
         # Aggregated data source information
         # { pathname: { desc=shortdesc, plugins=[plugin names] } }
@@ -497,34 +500,34 @@
         #for name, info in sorted(plugins.iteritems(), key=lambda x:x[0]):
         #    print "  %s:" % info["path"]
         #    print "    ", info["desc"]
-        print "Plugin status:"
-        maxname = max((len(x) for x in plugins.iterkeys()))
-        for name, info in sorted(plugins.iteritems(), key=lambda x:x[0]):
-            print " ", name.ljust(maxname), info["status"]
+        print("Plugin status:")
+        maxname = max((len(x) for x in plugins))
+        for name, info in sorted(plugins.items(), key=itemgetter(0)):
+            print(" ", name.ljust(maxname), info["status"])
 
         # Value information
-        print "Values:"
-        maxname = max((len(x) for x in values.iterkeys()))
-        print " ", "Value".ljust(maxname), "Code", "Provided by"
-        for name, val in sorted(values.iteritems(), key=lambda x:x[0]):
+        print("Values:")
+        maxname = max((len(x) for x in values))
+        print(" ", "Value".ljust(maxname), "Code", "Provided by")
+        for name, val in sorted(values.items(), key=itemgetter(0)):
             plugins = val.get("plugins", [])
             if plugins:
                 provider = ", ".join(plugins)
             else:
                 provider = "update-apt-xapian-index"
-            print " ", name.ljust(maxname), "%4d" % int(val["val"]), provider
+            print(" ", name.ljust(maxname), "%4d" % int(val["val"]), provider)
 
         # Source files information
-        print "Data sources:"
+        print("Data sources:")
         maxpath = 0
         maxdesc = 0
-        for k, v in sources.iteritems():
+        for k, v in sources.items():
             if len(k) > maxpath: maxpath = len(k)
             if len(v["desc"]) > maxdesc: maxdesc = len(v["desc"])
-        print " ", "Source".ljust(maxpath), "Description".ljust(maxdesc), "Used by"
-        for path, info in sources.iteritems():
+        print(" ", "Source".ljust(maxpath), "Description".ljust(maxdesc), "Used by")
+        for path, info in sources.items():
             provider = ", ".join(info.get("plugins", []))
-            print " ", path.ljust(maxpath), info["desc"].ljust(maxdesc), provider
+            print(" ", path.ljust(maxpath), info["desc"].ljust(maxdesc), provider)
 
         return 0
 
@@ -555,15 +558,15 @@
             terms.update((str(term) for term in self.db.db.synonym_keys(self.args[0])))
             terms.update((term.term for term in self.db.db.allterms(self.args[0])))
             for term in sorted(terms):
-                print term
+                print(term)
             for term in self.db.db.allterms("XT" + self.args[0]):
-                print term.term[2:]
+                print(term.term[2:])
             return 0
         elif self.opts.tabcomplete == "plain" and not self.args:
             # Show a preset list of tags
             for facet in ["interface", "role", "use", "works-with"]:
                 for term in self.db.db.allterms("XT" + facet + "::"):
-                    print term.term[2:]
+                    print(term.term[2:])
             return 0
         else:
             # Context-sensitive hints
@@ -584,8 +587,8 @@
 
         try:
             self.db.build_query()
-        except SavedStateError, e:
-            print >>sys.stderr, "%s: maybe you need to run '%s search' first?" % (self.name, str(e))
+        except SavedStateError as e:
+            print("%s: maybe you need to run '%s search' first?" % (self.name, str(e)), file=sys.stderr)
             return 1
 
         self.print_matches(self.db.get_matches(first = 0))
@@ -600,7 +603,7 @@
         self.db.set_query_args(qargs, secondary=True)
         try:
             self.db.build_query()
-        except SavedStateError, e:
+        except SavedStateError as e:
             return 0
         self.print_completions(self.db.get_matches(first = 0))
         return 0
@@ -610,8 +613,8 @@
         self.db = DB()
         try:
             self.db.build_query()
-        except SavedStateError, e:
-            print >>sys.stderr, "%s: maybe you need to run '%s search' first?" % (self.name, str(e))
+        except SavedStateError as e:
+            print("%s: maybe you need to run '%s search' first?" % (self.name, str(e)), file=sys.stderr)
             return 1
 
         count = int(args[0]) if args else 20
@@ -624,8 +627,8 @@
         self.db = DB()
         try:
             self.db.build_query()
-        except SavedStateError, e:
-            print >>sys.stderr, "%s: maybe you need to run '%s search' first?" % (self.name, str(e))
+        except SavedStateError as e:
+            print("%s: maybe you need to run '%s search' first?" % (self.name, str(e)), file=sys.stderr)
             return 1
 
         count = int(args[0]) if args else 20
@@ -657,15 +660,15 @@
                     pkg = m.document.get_data()
                     if partial is not None and not pkg.startswith(partial): continue
                     if pkg not in blacklist:
-                        print pkg
-            except SavedStateError, e:
+                        print(pkg)
+            except SavedStateError as e:
                 return 0
         else:
             # Prefix expand
             for term in self.db.db.allterms("XP" + (partial or "")):
                 pkg = term.term[2:]
                 if pkg not in blacklist:
-                    print pkg
+                    print(pkg)
         return 0
 
     def do_showpkg(self, args):
@@ -687,11 +690,11 @@
         "rdepends pkgname[s]: run apt-cache rdepends pkgname[s]"
         db = DB()
         for name in args:
-            print name
-            print "Reverse Depends:"
+            print(name)
+            print("Reverse Depends:")
             for pfx in ("XRD", "XRR", "XRS", "XRE", "XRP", "XRB", "XRC"):
                 for pkg in db.get_rdeps(name, pfx):
-                    print " ", pkg
+                    print(" ", pkg)
     complete_rdepends = complete_show
 
     def do_rdetails(self, args):
@@ -708,7 +711,7 @@
                     ("XRC", "con")):
                 deps = list(db.get_rdeps(name, pfx))
                 if not deps: continue
-                print name, tag, " ".join(deps)
+                print(name, tag, " ".join(deps))
     complete_rdetails = complete_show
 
     def do_policy(self, args):
@@ -722,22 +725,22 @@
     complete_madison = complete_show
 
     def format_help(self, out):
-        print >>out, "Commands:"
+        print("Commands:", file=out)
         itemlist = []
         maxusage = 0
-        for k, v in sorted(self.__class__.__dict__.iteritems()):
+        for k, v in sorted(self.__class__.__dict__.items()):
             if not k.startswith("do_"): continue
             line = self.name + " " + v.__doc__
             usage, desc = line.split(": ", 1)
             if len(usage) > maxusage:
                 maxusage = len(usage)
             itemlist.append([usage, desc])
-        print >>out, "  search commands:"
+        print("  search commands:", file=out)
         for usage, desc in [x for x in itemlist if "run apt-cache" not in x[1]]:
-            print >>out, "    %-*.*s  %s" % (maxusage, maxusage, usage, desc)
-        print >>out, "  apt-cache front-ends:"
+            print("    %-*.*s  %s" % (maxusage, maxusage, usage, desc), file=out)
+        print("  apt-cache front-ends:", file=out)
         for usage, desc in [x for x in itemlist if "run apt-cache" in x[1]]:
-            print >>out, "    %-*.*s  %s" % (maxusage, maxusage, usage, desc)
+            print("    %-*.*s  %s" % (maxusage, maxusage, usage, desc), file=out)
 
     def do_help(self, args):
         "help: show a summary of commands"
@@ -761,9 +764,9 @@
         exclude = [x for x in exclude if x.lower() not in self.BOOLWORDS]
         for s in self.clean_suggestions(self.db.get_suggestions(count=10, filter=DB.BasicFilter(stemmer=self.db.stem, exclude=exclude, prefix=prefix))):
             if s.startswith("tag:"):
-                print s[4:]
+                print(s[4:])
             else:
-                print s
+                print(s)
 
     def print_matches(self, matches):
         if self.opts.tags:
@@ -785,31 +788,31 @@
                 tag = self.db.unprefix(res.term)[4:]
                 desc = describe_tag(tag)
                 if desc is None:
-                    print "%i%% %s" % (int(score * 100), tag)
+                    print("%i%% %s" % (int(score * 100), tag))
                 else:
-                    print "%i%% %s -- %s" % (int(score * 100), tag, desc)
+                    print("%i%% %s -- %s" % (int(score * 100), tag, desc))
         else:
             est = matches.get_matches_estimated()
             first = matches.get_firstitem()
             count = matches.size()
-            print "%i results found." % est
+            print("%i results found." % est)
             if count != 0:
-                print "Results %i-%i:" % (first + 1, first + count)
+                print("Results %i-%i:" % (first + 1, first + count))
             elif first != 0:
-                print "No more results to show"
+                print("No more results to show")
             self.print_all_matches((m for m in matches))
             if first == 0:
                 sc = self.db.get_spelling_correction()
                 if sc:
-                    print "Did you mean:", sc, "?"
+                    print("Did you mean:", sc, "?")
                 sugg = self.clean_suggestions(self.db.get_suggestions(count=7, filter=DB.TermFilter(stemmer=self.db.stem, exclude=self.args)))
-                print "More terms:", " ".join(sugg)
+                print("More terms:", " ".join(sugg))
                 stags = self.clean_suggestions(self.db.get_suggestions(count=7, filter=DB.TagFilter()))
-                print "More tags:", " ".join([x[4:] for x in stags])
+                print("More tags:", " ".join([x[4:] for x in stags]))
             if first + count < est:
-                print "`%s more' will give more results" % self.name
+                print("`%s more' will give more results" % self.name)
             if first > 0:
-                print "`%s again' will restart the search" % self.name
+                print("`%s again' will restart the search" % self.name)
 
     def print_all_matches(self, matches_iter):
         """
@@ -824,9 +827,9 @@
             except KeyError:
                 pkg = None
             if pkg is not None and pkg.candidate:
-                print "%i%% %s - %s" % (m.percent, name, pkg.candidate.summary)
+                print("%i%% %s - %s" % (m.percent, name, pkg.candidate.summary))
             else:
-                print "%i%% %s - (unknown by apt)" % (m.percent, name)
+                print("%i%% %s - (unknown by apt)" % (m.percent, name))
 
     def perform(self):
         self.cmd = "help" if not self.args else self.args.pop(0)
@@ -838,7 +841,7 @@
         else:
             f = getattr(self, "do_" + self.cmd, None)
             if f is None:
-                print >>sys.stderr, "Invalid command: `%s'.\n" % self.cmd
+                print("Invalid command: `%s'.\n" % self.cmd, file=sys.stderr)
                 self.do_help(self.args)
                 return 1
             return f(self.args)
--- a/axi/__init__.py
+++ b/axi/__init__.py
@@ -18,6 +18,8 @@
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
 
+from __future__ import print_function
+
 import os
 import os.path
 import sys
@@ -59,7 +61,7 @@
             mo = re_value.match(line)
             if not mo:
                 if not quiet:
-                    print >>sys.stderr, "%s:%d: line is not `name value [# description]': ignored" % (pathname, idx+1)
+                    print("%s:%d: line is not `name value [# description]': ignored" % (pathname, idx+1), file=sys.stderr)
                 continue
             # Parse the number
             name = mo.group(1)
@@ -70,10 +72,10 @@
             descs[name] = desc
         if not values:
             raise BrokenIndexError
-    except (OSError, IOError, BrokenIndexError), e:
+    except (OSError, IOError, BrokenIndexError) as e:
         # If we can't read the database, fallback to defaults
         if not quiet:
-            print >>sys.stderr, "%s: %s. Falling back on a default value database" % (pathname, e)
+            print("%s: %s. Falling back on a default value database" % (pathname, e), file=sys.stderr)
         values = DEFAULT_VALUES
         descs = DEFAULT_VALUE_DESCS
     return values, descs
--- a/axi/indexer.py
+++ b/axi/indexer.py
@@ -19,6 +19,8 @@
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
 
+from __future__ import print_function
+
 import axi
 import sys
 import os
@@ -31,8 +33,10 @@
 import itertools
 import time
 import re
-import urllib
-import cPickle as pickle
+import pickle
+
+from operator import itemgetter
+from six.moves.urllib_parse import unquote
 
 APTLISTDIR="/var/lib/apt/lists"
 
@@ -59,7 +63,7 @@
                     self.info = self.obj.info(**kw)
                 except TypeError:
                     self.info = self.obj.info()
-            except Exception, e:
+            except Exception as e:
                 if progress:
                     progress.warning("Plugin %s initialisation failed: %s" % (fname, str(e)))
                 self.obj = None
@@ -105,7 +109,7 @@
         for f in os.listdir(APTLISTDIR):
             mo = tfile.search(f)
             if not mo: continue
-            langs.add(urllib.unquote(mo.group(1)))
+            langs.add(unquote(mo.group(1)))
 
         return langs
 
@@ -120,36 +124,36 @@
         self.is_verbose = False
     def begin(self, task):
         self.task = task
-        print "%s..." % self.task,
+        print("%s..." % self.task, end='')
         sys.stdout.flush()
         self.halfway = True
     def progress(self, percent):
-        print "\r%s... %d%%" % (self.task, percent),
+        print("\r%s... %d%%" % (self.task, percent), end='')
         sys.stdout.flush()
         self.halfway = True
     def end(self):
-        print "\r%s: done.  " % self.task
+        print("\r%s: done.  " % self.task)
         self.halfway = False
     def verbose(self, *args):
         if not self.is_verbose: return
         if self.halfway:
-            print
-        print " ".join(args)
+            print()
+        print(" ".join(args))
         self.halfway = False
     def notice(self, *args):
         if self.halfway:
-            print
-        print >>sys.stderr, " ".join(args)
+            print()
+        print(" ".join(args), file=sys.stderr)
         self.halfway = False
     def warning(self, *args):
         if self.halfway:
-            print
-        print >>sys.stderr, " ".join(args)
+            print()
+        print(" ".join(args), file=sys.stderr)
         self.halfway = False
     def error(self, *args):
         if self.halfway:
-            print
-        print >>sys.stderr, " ".join(args)
+            print()
+        print(" ".join(args), file=sys.stderr)
         self.halfway = False
 
 class BatchProgress:
@@ -160,25 +164,25 @@
         self.task = None
     def begin(self, task):
         self.task = task
-        print "begin: %s\n" % self.task,
+        print("begin: %s\n" % self.task, end='')
         sys.stdout.flush()
     def progress(self, percent):
-        print "progress: %d/100\n" % percent,
+        print("progress: %d/100\n" % percent, end='')
         sys.stdout.flush()
     def end(self):
-        print "done: %s\n" % self.task
+        print("done: %s\n" % self.task)
         sys.stdout.flush()
     def verbose(self, *args):
-        print "verbose: %s" % (" ".join(args))
+        print("verbose: %s" % (" ".join(args)))
         sys.stdout.flush()
     def notice(self, *args):
-        print "notice: %s" % (" ".join(args))
+        print("notice: %s" % (" ".join(args)))
         sys.stdout.flush()
     def warning(self, *args):
-        print "warning: %s" % (" ".join(args))
+        print("warning: %s" % (" ".join(args)))
         sys.stdout.flush()
     def error(self, *args):
-        print "error: %s" % (" ".join(args))
+        print("error: %s" % (" ".join(args)))
         sys.stdout.flush()
 
 class SilentProgress:
@@ -196,9 +200,9 @@
     def notice(self, *args):
         pass
     def warning(self, *args):
-        print >>sys.stderr, " ".join(args)
+        print(" ".join(args), file=sys.stderr)
     def error(self, *args):
-        print >>sys.stderr, " ".join(args)
+        print(" ".join(args), file=sys.stderr)
 
 class ClientProgress:
     """
@@ -304,7 +308,7 @@
         try:
             sock = self.sock.accept()[0]
             self.proxied.append(ServerSenderProgress(sock, self.task))
-        except socket.error, e:
+        except socket.error as e:
             if e.args[0] != errno.EAGAIN:
                 raise
         pass
@@ -339,13 +343,12 @@
     with ExecutionTime("db flush"):
         db.flush()
     """
-    import time
     def __init__(self, info=""):
         self.info = info
     def __enter__(self):
         self.now = time.time()
     def __exit__(self, type, value, stack):
-        print "%s: %s" % (self.info, time.time() - self.now)
+        print("%s: %s" % (self.info, time.time() - self.now))
 
 class Indexer(object):
     """
@@ -375,7 +378,7 @@
         try:
             # Try to create it anyway
             os.mkdir(pathname)
-        except OSError, e:
+        except OSError as e:
             if e.errno != errno.EEXIST:
                 # If we got an error besides path already existing, fail
                 raise
@@ -438,7 +441,7 @@
             # Wrap the current progress with the server sender
             self.progress = ServerProgress(self.progress)
             return True
-        except IOError, e:
+        except IOError as e:
             if e.errno == errno.EACCES or e.errno == errno.EAGAIN:
                 return False
             else:
@@ -484,7 +487,7 @@
                 cur_timestamp = os.path.getmtime(axi.XAPIANDBSTAMP)
             else:
                 cur_timestamp = 0
-        except OSError, e:
+        except OSError as e:
             cur_timestamp = 0
             self.progress.notice("Reading current timestamp failed: %s. Assuming the index has not been created yet." % e)
 
@@ -584,6 +587,14 @@
         seen = set()
         for fname in fnames:
             infd = open(fname)
+            # In Python 3, you cannot tell() on a file object while
+            # iterating so you have to pop the hood to get at the
+            # underlying buffer.  In Python 2, the file object doesn't have
+            # this attribute, or this restriction.
+            try:
+                tellable = infd.buffer
+            except AttributeError:
+                tellable = infd
             # Get file size to compute progress
             total = os.fstat(infd.fileno())[6]
             for idx, pkg in enumerate(deb822.Deb822.iter_paragraphs(infd)):
@@ -593,7 +604,7 @@
                 # Print approximate progress by checking the current read position
                 # against the file size
                 if total > 0 and idx % 200 == 0:
-                    cur = infd.tell()
+                    cur = tellable.tell()
                     self.progress.progress(100*cur/total)
                 yield self.get_document_from_deb822(pkg)
 
@@ -615,6 +626,9 @@
             if idx % 5000 == 0: self.progress.progress(100*idx/count)
             doc = db.get_document(m.docid)
             pkg = doc.get_data()
+            if bytes is not str and isinstance(pkg, bytes):
+                # python3-apt requires strings.
+                pkg = pkg.decode('utf-8')
             # this will return '' if there is no value 0, which is fine because it
             # will fail the comparison with the candidate version causing a reindex
             dbver = doc.get_value(0)
@@ -640,12 +654,12 @@
         """
         try:
             db = xapian.WritableDatabase(pathname, xapian.DB_CREATE_OR_OPEN)
-        except xapian.DatabaseLockError, e:
+        except xapian.DatabaseLockError:
             self.progress.warning("DB Update failed, database locked")
             return
 
         # Make sure the index CJK-compatible
-        if self.is_cjk_enabled() and db.get_metadata("cjk_ngram") != "1":
+        if self.is_cjk_enabled() and db.get_metadata("cjk_ngram") != b"1":
             self.progress.notice("The index %s is not CJK-compatible, rebuilding it" % axi.XAPIANINDEX)
             return self.rebuild()
 
@@ -747,9 +761,8 @@
         self.progress.verbose("Installing the new index.")
 
         #os.symlink(tmpidxfname, axi.XAPIANDBPATH + "/index.tmp")
-        out = open(axi.XAPIANINDEX + ".tmp", "w")
-        print >>out, "auto", os.path.abspath(dbdir)
-        out.close()
+        with open(axi.XAPIANINDEX + ".tmp", "w") as out:
+            print("auto", os.path.abspath(dbdir), file=out)
         os.rename(axi.XAPIANINDEX + ".tmp", axi.XAPIANINDEX)
 
         # Remove all other index.* directories that are not the newly created one
@@ -781,9 +794,8 @@
         Write the prefix information on the given file
         """
         self.progress.verbose("Writing prefix information to %s." % pathname)
-        out = open(pathname+".tmp", "w")
-
-        print >>out, textwrap.dedent("""
+        with open(pathname+".tmp", "w") as out:
+            print(textwrap.dedent("""
         # This file contains the information about keyword prefixes used in the
         # APT Xapian index.
         #
@@ -794,31 +806,30 @@
         # This file lists terms with their index prefix, their queryparser
         # prefix, whether queryparser should treat it as boolean or
         # probabilistic and a short description.
-        """).lstrip()
+        """).lstrip(), file=out)
 
-        # Aggregate and normalise prefix information from all the plugins
-        prefixes = dict()
-        for addon in self.plugins:
-            for p in addon.info.get("prefixes", []):
-                idx = p.get("idx", None)
-                if idx is None: continue
-                qp = p.get("qp", None)
-                type = p.get("type", None)
-                desc = p.get("desc", None)
-                # TODO: warn of inconsistencies (plugins that disagree on qp or type)
-                old = prefixes.setdefault(idx, dict())
-                if qp: old.setdefault("qp", qp)
-                if type: old.setdefault("type", type)
-                if desc: old.setdefault("desc", desc)
-
-        for name, info in sorted(prefixes.iteritems(), key=lambda x: x[0]):
-            print >>out, "%s\t%s\t%s\t# %s" % (
-                name,
-                info.get("qp", "-"),
-                info.get("type", "-"),
-                info.get("desc", "(description is missing)"))
+            # Aggregate and normalise prefix information from all the plugins
+            prefixes = dict()
+            for addon in self.plugins:
+                for p in addon.info.get("prefixes", []):
+                    idx = p.get("idx", None)
+                    if idx is None: continue
+                    qp = p.get("qp", None)
+                    type = p.get("type", None)
+                    desc = p.get("desc", None)
+                    # TODO: warn of inconsistencies (plugins that disagree on qp or type)
+                    old = prefixes.setdefault(idx, dict())
+                    if qp: old.setdefault("qp", qp)
+                    if type: old.setdefault("type", type)
+                    if desc: old.setdefault("desc", desc)
+
+            for name, info in sorted(prefixes.items(), key=itemgetter(0)):
+                print("%s\t%s\t%s\t# %s" % (
+                    name,
+                    info.get("qp", "-"),
+                    info.get("type", "-"),
+                    info.get("desc", "(description is missing)")), file=out)
 
-        out.close()
         # Atomic update of the documentation
         os.rename(pathname+".tmp", pathname)
 
@@ -827,9 +838,9 @@
         Write the value information on the given file
         """
         self.progress.verbose("Writing value information to %s." % pathname)
-        out = open(pathname+".tmp", "w")
+        with open(pathname+".tmp", "w") as out:
 
-        print >>out, textwrap.dedent("""
+            print(textwrap.dedent("""
         # This file contains the mapping between names of numeric values indexed in the
         # APT Xapian index and their index
         #
@@ -841,13 +852,12 @@
         # The format is exactly like /etc/services with name, number and optional
         # aliases, with the difference that the second column does not use the
         # "/protocol" part, which would be meaningless here.
-        """).lstrip()
+        """).lstrip(), file=out)
 
-        for name, idx in sorted(self.values.iteritems(), key=lambda x: x[1]):
-            desc = self.values_desc[name]
-            print >>out, "%s\t%d\t# %s" % (name, idx, desc)
+            for name, idx in sorted(self.values.items(), key=itemgetter(1)):
+                desc = self.values_desc[name]
+                print("%s\t%d\t# %s" % (name, idx, desc), file=out)
 
-        out.close()
         # Atomic update of the documentation
         os.rename(pathname+".tmp", pathname)
 
@@ -871,8 +881,8 @@
                 self.progress.notice("Skipping documentation for plugin", addon.filename)
 
         # Write the documentation in pathname
-        out = open(pathname+".tmp", "w")
-        print >>out, textwrap.dedent("""
+        with open(pathname+".tmp", "w") as out:
+            print(textwrap.dedent("""
         ===============
         Database layout
         ===============
@@ -888,12 +898,12 @@
         numeric values is found in ``%s``.
 
         The data sources used for indexing are:
-        """).lstrip() % (axi.XAPIANDBPATH, axi.XAPIANDBVALUES)
+        """).lstrip() % (axi.XAPIANDBPATH, axi.XAPIANDBVALUES), file=out)
 
-        for d in docinfo:
-            print >>out, " * %s: %s" % (d['name'], d['shortDesc'])
+            for d in docinfo:
+                print(" * %s: %s" % (d['name'], d['shortDesc']), file=out)
 
-        print >>out, textwrap.dedent("""
+                print(textwrap.dedent("""
         This Xapian index follows the conventions for term prefixes described in
         ``/usr/share/doc/xapian-omega/termprefixes.txt.gz``.
 
@@ -912,14 +922,13 @@
         Active data sources
         -------------------
 
-        """)
-        for d in docinfo:
-            print >>out, d['name']
-            print >>out, '='*len(d['name'])
-            print >>out, textwrap.dedent(d['fullDoc'])
-            print >>out
+        """), file=out)
+            for d in docinfo:
+                print(d['name'], file=out)
+                print('='*len(d['name']), file=out)
+                print(textwrap.dedent(d['fullDoc']), file=out)
+                print(file=out)
 
-        out.close()
         # Atomic update of the documentation
         os.rename(pathname+".tmp", pathname)
 
--- a/examples/aptxapianindex.py
+++ b/examples/aptxapianindex.py
@@ -4,6 +4,8 @@
 # To Public License, Version 2, as published by Sam Hocevar. See
 # http://sam.zoy.org/wtfpl/COPYING for more details.
 
+from __future__ import print_function
+
 import os, re
 import xapian
 
@@ -75,7 +77,7 @@
             # If a filter was requested, AND it with the query
             return xapian.Query(xapian.Query.OP_AND, filterdb[filtername], query)
         else:
-            raise RuntimeError("Invalid filter type.  Try one of " + ", ".join(sorted(filterdb.keys())))
+            raise RuntimeError("Invalid filter type.  Try one of " + ", ".join(sorted(filterdb)))
     else:
         return query
 
@@ -85,8 +87,8 @@
     """
     # Display the top 20 results, sorted by how well they match
     cache = apt.Cache()
-    print "%i results found." % mset.get_matches_estimated()
-    print "Results 1-%i:" % mset.size()
+    print("%i results found." % mset.get_matches_estimated())
+    print("Results 1-%i:" % mset.size())
     for m in mset:
         # /var/lib/apt-xapian-index/README tells us that the Xapian document data
         # is the package name.
@@ -98,7 +100,7 @@
 
         # Print the match, together with the short description
         if pkg.candidate:
-            print "%i%% %s - %s" % (m.percent, name, pkg.candidate.summary)
+            print("%i%% %s - %s" % (m.percent, name, pkg.candidate.summary))
 
 def readValueDB(pathname):
     """
@@ -116,23 +118,22 @@
             # Split the line
             fields = splitter.split(line)
             if len(fields) < 2:
-                print >>sys.stderr, "Ignoring line %s:%d: only 1 value found when I need at least the value name and number" % (pathname, idx+1)
+                print("Ignoring line %s:%d: only 1 value found when I need at least the value name and number" % (pathname, idx+1), file=sys.stderr)
                 continue
             # Parse the number
             try:
                 number = int(fields[1])
             except ValueError:
-                print >>sys.stderr, "Ignoring line %s:%d: the second column (\"%s\") must be a number" % (pathname, idx+1, fields[1])
+                print("Ignoring line %s:%d: the second column (\"%s\") must be a number" % (pathname, idx+1, fields[1]), file=sys.stderr)
                 continue
             values[fields[0]] = number
             for alias in fields[2:]:
                 values[alias] = number
-    except OSError, e:
+    except OSError as e:
         # If we can't read the database, fallback to defaults
-        print >>sys.stderr, "Cannot read %s: %s.  Using a minimal default configuration" % (pathname, e)
+        print("Cannot read %s: %s.  Using a minimal default configuration" % (pathname, e), file=sys.stderr)
         values = dict(
             installedsize = 1,
             packagesize = 2
         )
     return values
-
--- a/examples/axi-query-expand.py
+++ b/examples/axi-query-expand.py
@@ -18,6 +18,8 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
+from __future__ import print_function
+
 from optparse import OptionParser
 import sys
 
@@ -83,9 +85,9 @@
 # Print it out.  Note that some terms have a prefix from the database: can we
 # filter them out?  Indeed: Xapian allow to give a filter to get_eset.
 # Read on...
-print
-print "Terms that could improve the search:",
-print ", ".join(["%s (%.2f%%)" % (res.term, res.weight) for res in eset])
+print()
+print("Terms that could improve the search:", end='')
+print(", ".join(["%s (%.2f%%)" % (res.term, res.weight) for res in eset]))
 
 
 # You can also abuse this feature to show what are the tags that are most
@@ -108,8 +110,8 @@
 eset = enquire.get_eset(10, rset, Filter())
 
 # Print out the resulting tags
-print
-print "Tags that could improve the search:",
-print ", ".join(["%s (%.2f%%)" % (res.term[2:], res.weight) for res in eset])
+print()
+print("Tags that could improve the search:", end='')
+print(", ".join(["%s (%.2f%%)" % (res.term[2:], res.weight) for res in eset]))
 
 sys.exit(0)
--- a/examples/axi-query-pkgtype.py
+++ b/examples/axi-query-pkgtype.py
@@ -19,6 +19,8 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
+from __future__ import print_function
+
 from optparse import OptionParser
 import sys
 
@@ -79,7 +81,7 @@
         # If a filter was requested, AND it with the query
         query = xapian.Query(xapian.Query.OP_AND, query, filterdb[options.type])
     else:
-        print >>sys.stderr, "Invalid filter type.  Try one of", ", ".join(sorted(filterdb.keys()))
+        print("Invalid filter type.  Try one of", ", ".join(sorted(filterdb)), file=sys.stderr)
         sys.exit(1)
 
 
@@ -90,8 +92,8 @@
 # Display the top 20 results, sorted by how well they match
 cache = apt.Cache()
 matches = enquire.get_mset(0, 20)
-print "%i results found." % matches.get_matches_estimated()
-print "Results 1-%i:" % matches.size()
+print("%i results found." % matches.get_matches_estimated())
+print("Results 1-%i:" % matches.size())
 for m in matches:
     # /var/lib/apt-xapian-index/README tells us that the Xapian document data
     # is the package name.
@@ -103,6 +105,6 @@
 
     if pkg.candidate:
     # Print the match, together with the short description
-        print "%i%% %s - %s" % (m.percent, name, pkg.candidate.summary)
+        print("%i%% %s - %s" % (m.percent, name, pkg.candidate.summary))
 
 sys.exit(0)
--- a/examples/axi-query-simple.py
+++ b/examples/axi-query-simple.py
@@ -18,6 +18,8 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
+from __future__ import print_function
+
 from optparse import OptionParser
 import sys
 
@@ -106,8 +108,8 @@
 # Display the top 20 results, sorted by how well they match
 cache = apt.Cache()
 matches = enquire.get_mset(0, 20)
-print "%i results found." % matches.get_matches_estimated()
-print "Results 1-%i:" % matches.size()
+print("%i results found." % matches.get_matches_estimated())
+print("Results 1-%i:" % matches.size())
 for m in matches:
     # /var/lib/apt-xapian-index/README tells us that the Xapian document data
     # is the package name.
@@ -119,6 +121,6 @@
 
     if pkg.candidate:
         # Print the match, together with the short description
-        print "%i%% %s - %s" % (m.percent, name, pkg.candidate.summary)
+        print("%i%% %s - %s" % (m.percent, name, pkg.candidate.summary))
 
 sys.exit(0)
--- a/examples/axi-query-tags.py
+++ b/examples/axi-query-tags.py
@@ -18,6 +18,8 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
+from __future__ import print_function
+
 from optparse import OptionParser
 import sys
 
@@ -91,6 +93,6 @@
 
 # Print out the results
 for res in eset:
-    print "%.2f %s" % (res.weight, res.term[2:])
+    print("%.2f %s" % (res.weight, res.term[2:]))
 
 sys.exit(0)
--- a/examples/axi-query.py
+++ b/examples/axi-query.py
@@ -20,6 +20,8 @@
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
 
+from __future__ import print_function
+
 from aptxapianindex import *
 from optparse import OptionParser
 import sys
@@ -96,12 +98,12 @@
 # Display the results.
 cache = apt.Cache()
 matches = enquire.get_mset(0, 20)
-print "%i results found." % matches.get_matches_estimated()
-print "Results 1-%i:" % matches.size()
+print("%i results found." % matches.get_matches_estimated())
+print("Results 1-%i:" % matches.size())
 for m in matches:
     name = m.document.get_data()
     pkg = cache[name]
     if pkg.candidate:
-        print "%i%% %s - %s" % (m.percent, name, pkg.candidate.summary)
+        print("%i%% %s - %s" % (m.percent, name, pkg.candidate.summary))
 
 sys.exit(0)
--- a/plugins/app-install.py
+++ b/plugins/app-install.py
@@ -3,10 +3,9 @@
     from xdg.Exceptions import ParsingError
     from xdg import Locale
     HAS_XDG=True
-except ImportError, e:
+except ImportError as e:
     HAS_XDG=False
 
-import axi.indexer
 import xapian
 import os, os.path
 
@@ -55,7 +54,7 @@
         # Add an "app-popcon" value with popcon rank
         try:
             popcon = int(entry.get("X-AppInstall-Popcon"))
-        except ValueError, e:
+        except ValueError as e:
             if self.progress:
                 self.progress.verbose("%s: parsing X-AppInstall-Popcon: %s" % (fname, str(e)))
             popcon = -1
--- a/plugins/cataloged_time.py
+++ b/plugins/cataloged_time.py
@@ -3,7 +3,7 @@
     HAS_APT=True
 except ImportError:
     HAS_APT=False
-import cPickle
+import pickle
 import os
 import os.path
 import time
@@ -69,7 +69,7 @@
             self._packages_cataloged_file = None
         if (self._packages_cataloged_file and 
             os.path.exists(self._packages_cataloged_file)):
-            self._package_cataloged_time = cPickle.load(open(self._packages_cataloged_file))
+            self._package_cataloged_time = pickle.load(open(self._packages_cataloged_file, 'rb'))
         else:
             self._package_cataloged_time = {}
         self.now = time.time()
@@ -123,8 +123,8 @@
         Called when the indexing is finihsed
         """
         if self._packages_cataloged_file:
-            f=open(self._packages_cataloged_file+".new", "w")
-            res = cPickle.dump(self._package_cataloged_time, f)
+            f=open(self._packages_cataloged_file+".new", "wb")
+            res = pickle.dump(self._package_cataloged_time, f)
             f.close()
             os.rename(self._packages_cataloged_file+".new",
                       self._packages_cataloged_file)
--- a/plugins/descriptions.py
+++ b/plugins/descriptions.py
@@ -129,7 +129,7 @@
             self.indexer.index_text_without_positions(pkg["Description"])
         else:
             # check if we have a translated description
-            for k in pkg.keys():
+            for k in pkg:
                 if k.startswith('Description-'):
                     self.indexer.index_text_without_positions(pkg[k])
                     break
--- a/plugins/translated-desc.py
+++ b/plugins/translated-desc.py
@@ -5,12 +5,15 @@
     HAS_APT=False
 import xapian
 import re
-import os, os.path, urllib
+import os
+import codecs
 try:
     from debian import deb822
 except ImportError:
     from debian_bundle import deb822
 
+from six.moves.urllib_parse import unquote
+
 APTLISTDIR="/var/lib/apt/lists"
 
 def translationFiles(langs=None):
@@ -21,7 +24,7 @@
         mo = tfile.search(f)
         if not mo: continue
         if langs and not mo.group(1) in langs: continue
-        yield urllib.unquote(mo.group(1)), os.path.join(APTLISTDIR, f)
+        yield unquote(mo.group(1)), os.path.join(APTLISTDIR, f)
 
 class Indexer:
     def __init__(self, lang, file):
@@ -38,13 +41,14 @@
         # Read the translated descriptions
         self.descs = dict()
         desckey = "Description-"+self.lang
-        for pkg in deb822.Deb822.iter_paragraphs(open(file)):
-            # I need this if because in some translation files, some packages
-            # have a different Description header.  For example, in the -de
-            # translations, I once found a Description-de.noguide: header
-            # instead of Description-de:
-            if desckey in pkg:
-                self.descs[pkg["Package"]] = pkg[desckey]
+        with codecs.open(file, 'r', encoding='utf-8') as fp:
+            for pkg in deb822.Deb822.iter_paragraphs(fp):
+                # I need this if because in some translation files, some
+                # packages have a different Description header.  For example,
+                # in the -de translations, I once found a
+                # Description-de.noguide: header instead of Description-de:
+                if desckey in pkg:
+                    self.descs[pkg["Package"]] = pkg[desckey]
 
     def index(self, document):
         name = document.get_data()
--- a/runtests
+++ b/runtests
@@ -15,6 +15,6 @@
 		rm -rf test/test-coverage test/.coverage testdb
 		;;
 	*)
-		nosetests -w test "$@"
+		python -m nose -w test "$@"
 		;;
 esac
--- a/setup.py
+++ b/setup.py
@@ -15,9 +15,9 @@
       author=['Enrico Zini'],
       author_email=['enrico@debian.org'],
       url='http://www.enricozini.org/sw/apt-xapian-index/',
-      install_requires = [
-          "debian", "apt", "xapian",
-      ],
+      ## install_requires = [
+      ##     "debian", "apt", "xapian",
+      ## ],
       license='GPL',
       platforms='any',
       packages=['axi'],
--- a/test/test_indexer.py
+++ b/test/test_indexer.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 import unittest
-import sys, os.path
+import os
 import axi
 import axi.indexer
 import shutil
@@ -26,7 +26,7 @@
 
         def __getitem__(self, name):
             if name not in self._pkgs:
-                raise KeyError, "`%s' not in wrapped cache" % name
+                raise KeyError("`%s' not in wrapped cache" % name)
             return self._cache[name]
 
     return sc
@@ -167,10 +167,10 @@
 
         # Ensure that we created a new index (because we reindexed to get CJK)
         self.assertNotEqual(open(axi.XAPIANINDEX).read(), curidx)
-        
+
         # Double check that we set the CJK flag in the index
         db = xapian.Database(axi.XAPIANINDEX)
-        self.assertEqual(db.get_metadata("cjk_ngram"), "1")
+        self.assertEqual(db.get_metadata("cjk_ngram"), b"1")
 
     def testIncrementalRebuildFromEmpty(self):
         # Prepare an incremental update
--- a/tests/dbus-update-apt-xapian-index.py
+++ b/tests/dbus-update-apt-xapian-index.py
@@ -1,5 +1,7 @@
 #!/usr/bin/python
 
+from __future__ import print_function
+
 import dbus
 import os
 import glib
@@ -7,10 +9,10 @@
 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
 
 def finished(res):
-	print "finished: ", res
+    print("finished: ", res)
 
 def progress(percent):
-	print "progress: ", percent
+    print("progress: ", percent)
 
 system_bus = dbus.SystemBus()
 
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,16 @@
+[tox]
+envlist = {nocov,cov}-{py27,py34,py35}
+
+[testenv]
+sitepackages = True
+indexserver =
+    default = http://missing.example.com
+usedevelop = True
+setenv =
+       AXI_PLUGIN_DIR={toxinidir}/plugins
+       AXI_DB_PATH={toxinidir}/testdb
+       AXI_CACHE_PATH={toxinidir}/testdb
+       PYTHONPATH="$PYTHONPATH:{toxinidir}"
+commands =
+    nocov: python -m nose -w test -vv
+    cov: python -m nose -w test --with-coverage --cover-package axi --cover-html --cover-html-dir=test-coverage-{envname}
--- a/update-apt-xapian-index
+++ b/update-apt-xapian-index
@@ -22,6 +22,8 @@
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
 
+from __future__ import print_function
+
 import os
 # Activate support for the CJK tokenizer
 os.environ["XAPIAN_CJK_NGRAM"] = "1"
@@ -88,10 +90,10 @@
 # Lock the session so that we prevent concurrent updates
 try:
     locked = indexer.lock()
-except OSError, e:
+except OSError as e:
     import errno
     if e.errno == errno.EACCES:
-        print >>sys.stderr, "You probably need to be root to do this."
+        print("You probably need to be root to do this.", file=sys.stderr)
         sys.exit(1)
     raise
 if not locked:
--- a/update-apt-xapian-index-dbus
+++ b/update-apt-xapian-index-dbus
@@ -11,7 +11,8 @@
     import dbus
     import dbus.service
     import dbus.mainloop.glib
-except ImportError, e:
+except ImportError as e:
+    import sys
     sys.stderr.write("Failed to import '%s', can not use dbus" % e)
     sys.exit(1)
 
@@ -86,7 +87,7 @@
     def update_async(self, force, update_only, sender=None, conn=None):
         if not self._authWithPolicyKit(sender, conn, 
                                        "org.debian.aptxapianindex.update"):
-            raise PermissionDeniedError, "Permission denied by policy"
+            raise PermissionDeniedError("Permission denied by policy")
         # do not start update-apt-xapian-index twice, the clients will
         # get the finished signal from the previous running one
         if self._active_axi:
