From 3e28fbe5d2cb4d3c80b8d79218a5ac081c067e7e Mon Sep 17 00:00:00 2001
From: Ralph Bean <rbean@redhat.com>
Date: Mon, 25 Jun 2012 22:50:24 -0400
Subject: [PATCH] Python3 support.

Use python-six to support both pythons 2 and 3 simultaneously.
All tests pass on both python2.7 and python3.2.
---
 THANKS                        |    1 +
 bin/bugzilla                  |  106 ++++++++++++++++++++------------------
 bugzilla/__init__.py          |   22 +++++---
 bugzilla/base.py              |  112 ++++++++++++++++++++++++++++-------------
 bugzilla/bugzilla3.py         |   12 +++--
 bugzilla/bugzilla4.py         |    2 +
 bugzilla/nvlbugzilla.py       |   24 +++++++--
 bugzilla/rhbugzilla.py        |   24 +++++----
 bugzilla/util.py              |   10 ++--
 tests/__init__.py             |   12 ++---
 tests/manualtests/selftest.py |   71 +++++++++++++-------------
 11 files changed, 240 insertions(+), 156 deletions(-)

diff --git a/THANKS b/THANKS
index 84c2f3e..5bbc901 100644
--- a/THANKS
+++ b/THANKS
@@ -8,6 +8,7 @@ Toshio Kuratomi <toshio@fedoraproject.org>
 Michal Vyskocil <mvyskocil@suse.cz>
 Eduardo Habkost <ehabkost@redhat.com>
 Jarod Wilson <jarod@redhat.com>
+Ralph Bean <rbean@redhat.com>
 
 If presented with the opportunity, you should buy them a beer (or some other
 beverage of their choice).
diff --git a/bin/bugzilla b/bin/bugzilla
index 49e4a09..d3109f9 100755
--- a/bin/bugzilla
+++ b/bin/bugzilla
@@ -9,6 +9,7 @@
 # Free Software Foundation; either version 2 of the License, or (at your
 # option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
 # the full text of the license.
+from __future__ import print_function
 
 import bugzilla, optparse
 import os, sys, re
@@ -18,8 +19,15 @@ import bugzilla.util
 import locale
 import pprint
 import socket
-import xmlrpclib
+
+try:
+    import xmlrpclib
+except ImportError:
+    import xmlrpc.client as xmlrpclib
+
+
 from tempfile import NamedTemporaryFile
+import six
 
 version = '0.7.0'
 default_bz = 'https://bugzilla.redhat.com/xmlrpc.cgi'
@@ -37,11 +45,11 @@ cmdlist = ['login','new','query','modify','attach','info']
 _is_unittest = bool(os.getenv("__BUGZILLA_UNITTEST"))
 
 def to_encoding(ustring):
-    if isinstance(ustring, basestring):
-        if isinstance(ustring, unicode):
+    if isinstance(ustring, six.string_types):
+        if isinstance(ustring, six.text_type):
             return ustring.encode(locale.getpreferredencoding(), 'replace')
         return ustring
-    return u''
+    return six.u('')
 
 format_field_re = re.compile("%{([a-z0-9_]+)(?::([^}]*))?}")
 
@@ -343,7 +351,7 @@ everything that's exposed by XML-RPC is used by
 Bugs? In a sub-1.0 release? Preposterous.
 .SH AUTHOR
 Will Woods <wwoods@redhat.com>'''
-    print manpage
+    print(manpage)
 
 def do_query(bz, opt, parser):
     # Construct the query from the list of queryable options
@@ -523,10 +531,10 @@ def main(bzinstance=None):
         sys.stdout.write('Logging in... ')
         sys.stdout.flush()
         if bz.login(global_opt.user,global_opt.password):
-            print 'Authorization cookie received.'
+            print('Authorization cookie received.')
             sys.exit(0)
         else:
-            print 'failed.'
+            print('failed.')
             sys.exit(1)
 
     # Set up authentication
@@ -565,13 +573,13 @@ def main(bzinstance=None):
         if opt.component_owners:
             component_details = bz.getcomponentsdetails(opt.component_owners)
             for c in sorted(component_details):
-                print to_encoding(u"%s: %s" % (c, component_details[c]['initialowner']))
+                print(to_encoding(six.u("%s: %s") % (c, component_details[c]['initialowner'])))
 
         if opt.versions:
             for p in bz.querydata['product']:
                 if p['name'] == opt.versions:
                     for v in p['versions']:
-                        print to_encoding(v)
+                        print(to_encoding(v))
 
     elif action == 'query':
         buglist = do_query(bz, opt, parser)
@@ -609,7 +617,7 @@ def main(bzinstance=None):
                 while data:
                     outfile.write(data)
                     data = att.read(4096)
-                print "Wrote %s" % outfile.name
+                print("Wrote %s" % outfile.name)
             return
         if not opt.file and opt.desc:
             parser.error("attaching a file requires --file and --desc")
@@ -639,7 +647,7 @@ def main(bzinstance=None):
                                   filename=opt.file,
                                   contenttype=opt.type,
                                   ispatch=(opt.type == "text/x-patch"))
-            print "Created attachment %i on bug %s" % (attid, bugid)
+            print("Created attachment %i on bug %s" % (attid, bugid))
 
     elif action == 'modify':
         if not (opt.status or opt.close or opt.assignee or opt.flag or opt.cc
@@ -705,7 +713,7 @@ def main(bzinstance=None):
                 data['fixed_in'] = opt.fixed_in
             bz._update_bugs(bugid_list, data)
     else:
-        print "Sorry - '%s' not implemented yet." % action
+        print("Sorry - '%s' not implemented yet." % action)
 
     # If we're doing new/query/modify, output our results
     if action in ['new','query']:
@@ -717,7 +725,7 @@ def main(bzinstance=None):
             def bug_field(matchobj):
                 (fieldname, rest) = matchobj.groups()
 
-                if special_fields.has_key(fieldname):
+                if fieldname in special_fields:
                     val = special_fields[fieldname](b, rest)
                 else:
                     val = getattr(b,fieldname)
@@ -728,18 +736,18 @@ def main(bzinstance=None):
                 except UnicodeEncodeError:
                     return to_encoding(val)
             for b in buglist:
-                print format_field_re.sub(bug_field,opt.outputformat)
+                print(format_field_re.sub(bug_field,opt.outputformat))
         elif opt.output == 'ids':
             for b in buglist:
-                print b.bug_id
+                print(b.bug_id)
         elif opt.output == 'full':
             fullbuglist = bz.getbugs([b.bug_id for b in buglist])
             for b in fullbuglist:
-                print to_encoding(u'#%s %s - %s - %s' %
-                        (b.bug_id, b.bug_status, b.assigned_to, b.short_desc))
-                if b.cc: print "CC: %s" % " ".join(b.cc)
-                if b.blocked: print "Blocked: %s" % " ".join([str(i) for i in b.blocked])
-                if b.dependson: print "Depends: %s" % " ".join([str(i) for i in b.dependson])
+                print(to_encoding(six.u('#%s %s - %s - %s') %
+                        (b.bug_id, b.bug_status, b.assigned_to, b.short_desc)))
+                if b.cc: print("CC: %s" % " ".join(b.cc))
+                if b.blocked: print("Blocked: %s" % " ".join([str(i) for i in b.blocked]))
+                if b.dependson: print("Depends: %s" % " ".join([str(i) for i in b.dependson]))
                 for c in b.longdescs:
                     if 'email' in c:
                         email = c['email']
@@ -747,35 +755,35 @@ def main(bzinstance=None):
                         email = c['safe_email']
                     else:
                         email = 'Unknown'
-                    print to_encoding(u"* %s - %s (%s):\n%s\n" % (c['time'],
-                        c['author'], email, c['body']))
+                    print(to_encoding(six.u("* %s - %s (%s):\n%s\n") % (c['time'],
+                        c['author'], email, c['body'])))
         elif opt.output == 'normal':
             for b in buglist:
-                print to_encoding(u'#%s %8s - %s - %s' %
-                        (b.bug_id, b.bug_status, b.assigned_to, b.short_desc))
+                print(to_encoding(six.u('#%s %8s - %s - %s') %
+                        (b.bug_id, b.bug_status, b.assigned_to, b.short_desc)))
 
         elif opt.output == 'extra':
-            print "Grabbing 'extra' bug information. This could take a moment."
+            print("Grabbing 'extra' bug information. This could take a moment.")
             fullbuglist = bz.getbugs([b.bug_id for b in buglist])
             for b in fullbuglist:
-                print to_encoding(u'#%s %s - %s - %s' %
-                        (b.bug_id, b.bug_status, b.assigned_to, b.short_desc))
+                print(to_encoding(six.u('#%s %s - %s - %s') %
+                        (b.bug_id, b.bug_status, b.assigned_to, b.short_desc)))
                 if hasattr(b, 'keywords') and b.keywords:
-                    print to_encoding(u" +Keywords: %s" % b.keywords)
+                    print(to_encoding(six.u(" +Keywords: %s") % b.keywords))
                 if hasattr(b, 'qa_whiteboard') and b.qa_whiteboard:
-                    print to_encoding(u" +QA Whiteboard: %s" % b.qa_whiteboard)
+                    print(to_encoding(six.u(" +QA Whiteboard: %s") % b.qa_whiteboard))
                 if hasattr(b, 'status_whiteboard') and b.status_whiteboard:
-                    print to_encoding(u" +Status Whiteboard: %s" %
-                            b.status_whiteboard)
+                    print(to_encoding(six.u(" +Status Whiteboard: %s") %
+                            b.status_whiteboard))
                 if hasattr(b, 'devel_whiteboard') and b.devel_whiteboard:
-                    print to_encoding(u" +Devel Whiteboard: %s" %
-                            b.devel_whiteboard)
-            print "\nBugs listed: ",len(buglist)
+                    print(to_encoding(six.u(" +Devel Whiteboard: %s") %
+                            b.devel_whiteboard))
+            print("\nBugs listed: ",len(buglist))
         elif opt.output == 'oneline':
             for b in buglist:
                 flags=''
                 cve=''
-		flags += "".join([str(i) for i in b.flags.split(',')])
+                flags += "".join([str(i) for i in b.flags.split(',')])
                 #grab CVEs by searching the keywords and grabbing that bugzilla
                 if b.keywords.find("Security") != -1:
                     blockedby="%s" % b.blockedby
@@ -787,16 +795,16 @@ def main(bzinstance=None):
                 # bugzilla.redhat.com has component as a list
                 if type(b.component) == list:
                     b.component = ','.join(b.component)
-                print to_encoding(u"#%s %8s %22s %s\t[%s] %s %s" %
+                print(to_encoding(six.u("#%s %8s %22s %s\t[%s] %s %s") %
                         (b.bug_id, b.bug_status, b.assigned_to, b.component,
-                            b.target_milestone, flags, cve))
+                            b.target_milestone, flags, cve)))
         elif opt.output == 'raw':
             fullbuglist = bz.getbugs([b.bug_id for b in buglist])
             for b in fullbuglist:
-                print "Bugzilla %s: " % b.bug_id
+                print("Bugzilla %s: " % b.bug_id)
                 for a in dir(b):
-                    print to_encoding(u"ATTRIBUTE[%s]: %s" % (a, getattr(b, a)))
-                print "\n\n"
+                    print(to_encoding(six.u("ATTRIBUTE[%s]: %s") % (a, getattr(b, a))))
+                print("\n\n")
         else:
             parser.error("opt.output was set to something weird.")
 
@@ -807,18 +815,18 @@ if __name__ == '__main__':
         else:
             main()
     except KeyboardInterrupt:
-        print "\ninterrupted."
+        print("\ninterrupted.")
         sys.exit(1)
-    except socket.error, e:
-        print "\nConnection lost/failed: %s" % str(e)
+    except socket.error as e:
+        print("\nConnection lost/failed: %s" % str(e))
         sys.exit(2)
-    except xmlrpclib.Fault, e:
-        print "\nServer error: %s" % str(e)
+    except xmlrpclib.Fault as e:
+        print("\nServer error: %s" % str(e))
         sys.exit(3)
-    except xmlrpclib.ProtocolError, e:
-        print "\nInvalid server response: %d %s" % (e.errcode, e.errmsg)
+    except xmlrpclib.ProtocolError as e:
+        print("\nInvalid server response: %d %s" % (e.errcode, e.errmsg))
         redir = e.headers.getheader("location",0)
         if redir:
-            print "Server was attempting a redirect."
-            print 'Try "bugzilla --bugzilla %s ..."' % redir
+            print("Server was attempting a redirect.")
+            print('Try "bugzilla --bugzilla %s ..."' % redir)
         sys.exit(4)
diff --git a/bugzilla/__init__.py b/bugzilla/__init__.py
index 941ea21..8cf2752 100644
--- a/bugzilla/__init__.py
+++ b/bugzilla/__init__.py
@@ -9,12 +9,18 @@
 # option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
 # the full text of the license.
 
-from bugzilla3 import Bugzilla3, Bugzilla32, Bugzilla34, Bugzilla3
-from bugzilla4 import Bugzilla4
-from rhbugzilla import RHBugzilla, RHBugzilla3, RHBugzilla4
-from nvlbugzilla import NovellBugzilla
-from base import version
-import xmlrpclib
+from .bugzilla3 import Bugzilla3, Bugzilla32, Bugzilla34, Bugzilla3
+from .bugzilla4 import Bugzilla4
+from .rhbugzilla import RHBugzilla, RHBugzilla3, RHBugzilla4
+from .nvlbugzilla import NovellBugzilla
+from .base import version
+
+try:
+    import xmlrpclib
+except ImportError:
+    # Python 3
+    import xmlrpc.client as xmlrpclib
+
 import logging
 log = logging.getLogger("bugzilla")
 
@@ -78,6 +84,6 @@ class Bugzilla(object):
                 c.__init__(self,**kwargs)
                 log.info("Chose subclass %s v%s" % (c.__name__,c.version))
             else:
-                raise ValueError, "Couldn't determine Bugzilla version for %s" % kwargs['url']
+                raise ValueError("Couldn't determine Bugzilla version for %s" % kwargs['url'])
         else:
-            raise TypeError, "You must pass a valid bugzilla xmlrpc.cgi URL"
+            raise TypeError("You must pass a valid bugzilla xmlrpc.cgi URL")
diff --git a/bugzilla/base.py b/bugzilla/base.py
index 7748e2b..38136a8 100644
--- a/bugzilla/base.py
+++ b/bugzilla/base.py
@@ -9,11 +9,35 @@
 # option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
 # the full text of the license.
 
-import xmlrpclib, urllib2
 try:
-       import cookielib
+    import xmlrpclib
+    import urllib2
+    from urllib2 import (
+        HTTPCookieProcessor,
+        build_opener,
+        Request,
+    )
 except ImportError:
-       import ClientCookie as cookielib
+    # Python 3
+    import xmlrpc.client as xmlrpclib
+    import urllib as urllib2
+    urllib2.__version__ = "__unversioned__"
+    from urllib.request import (
+        HTTPCookieProcessor,
+        build_opener,
+        Request,
+    )
+
+try:
+    import cookielib
+except ImportError:
+    try:
+        import ClientCookie as cookielib
+    except ImportError:
+        # Python 3
+        import http.cookiejar as cookielib
+
+import six
 import os
 import base64
 import tempfile
@@ -28,6 +52,7 @@ version = '0.7.0'
 user_agent = 'Python-urllib2/%s bugzilla.py/%s' % \
         (urllib2.__version__,version)
 
+
 class BugzillaError(Exception):
     '''Error raised in the Bugzilla client code.'''
     pass
@@ -153,7 +178,7 @@ class BugzillaBase(object):
         """
         try:
             cj.load(self._cookiefile)
-        except cookielib.LoadError, le:
+        except cookielib.LoadError as le:
             cj = cookielib.MozillaCookieJar(self._cookiefile)
             cj.load(self._cookiefile)
 
@@ -177,7 +202,7 @@ class BugzillaBase(object):
             self._cookiefobj = tmpfile
             try:
                 cj.save(self._cookiefile)
-            except Exception, e:
+            except Exception as e:
                 log.warn("Couldn't initialize temporary cookiefile%s: %s" %
                         (self._cookiefile, str(e)))
         else:
@@ -185,10 +210,10 @@ class BugzillaBase(object):
                 cj = self._loadcookies(cj)
             else:
                 # Create an empty cookiefile that's only readable by this user
-                old_umask = os.umask(0077)
+                old_umask = os.umask(0o077)
                 try:
                     cj.save(self._cookiefile)
-                except Exception, e:
+                except Exception as e:
                     log.error("Couldn't initialize cookiefile %s: %s" %
                             (self._cookiefile, str(e)))
                 os.umask(old_umask)
@@ -221,7 +246,12 @@ class BugzillaBase(object):
     configpath = ['/etc/bugzillarc','~/.bugzillarc']
     def readconfig(self,configpath=None):
         '''Read bugzillarc file(s) into memory.'''
-        import ConfigParser
+        try:
+            import ConfigParser
+        except ImportError:
+            # Python 3
+            import configparser as ConfigParser
+
         if not configpath:
             configpath = self.configpath
         configpath = [os.path.expanduser(p) for p in configpath]
@@ -269,8 +299,8 @@ class BugzillaBase(object):
         # Set up the proxy, using the transport
         self._proxy = xmlrpclib.ServerProxy(url,self._transport)
         # Set up the urllib2 opener (using the same cookiejar)
-        handler = urllib2.HTTPCookieProcessor(self._cookiejar)
-        self._opener = urllib2.build_opener(handler)
+        handler = HTTPCookieProcessor(self._cookiejar)
+        self._opener = build_opener(handler)
         self._opener.addheaders = [('User-agent',self.user_agent)]
         self.url = url
         self.readconfig() # we've changed URLs - reload config
@@ -309,16 +339,16 @@ class BugzillaBase(object):
             self.password = password
 
         if not self.user:
-            raise ValueError, "missing username"
+            raise ValueError("missing username")
         if not self.password:
-            raise ValueError, "missing password"
+            raise ValueError("missing password")
 
         try:
             r = self._login(self.user,self.password)
             self.logged_in = True
             log.info("login successful - dropping password from memory")
             self.password = ''
-        except xmlrpclib.Fault, f:
+        except xmlrpclib.Fault as f:
             r = False
         return r
 
@@ -352,7 +382,7 @@ class BugzillaBase(object):
             log.debug("Refreshing bugfields")
             try:
                 self._bugfields = self._getbugfields()
-            except xmlrpclib.Fault, f:
+            except xmlrpclib.Fault as f:
                 if f.faultCode == 'Client':
                     # okay, this instance doesn't have getbugfields. fine.
                     self._bugfields = []
@@ -413,13 +443,13 @@ class BugzillaBase(object):
         for p in self.products:
             if p['id'] == productid:
                 return p['name']
-        raise ValueError, 'No product with id #%i' % productid
+        raise ValueError('No product with id #%i' % productid)
     def _product_name_to_id(self,product):
         '''Convert a product name (str) to a product ID (int).'''
         for p in self.products:
             if p['name'] == product:
                 return p['id']
-        raise ValueError, 'No product named "%s"' % product
+        raise ValueError('No product named "%s"' % product)
 
     #---- Methods for retrieving Components
 
@@ -681,7 +711,7 @@ class BugzillaBase(object):
         elif hasattr(attachfile,'read'):
             f = attachfile
         else:
-            raise TypeError, "attachfile must be filename or file-like object"
+            raise TypeError("attachfile must be filename or file-like object")
         kwargs['description'] = description
         if 'filename' not in kwargs:
             kwargs['filename'] = os.path.basename(f.name)
@@ -702,7 +732,7 @@ class BugzillaBase(object):
         '''Get the contents of the attachment with the given attachment ID.
         Returns a file-like object.'''
         att_uri = self._attachment_uri(attachid)
-        opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self._cookiejar))
+        opener = build_opener(HTTPCookieProcessor(self._cookiejar))
         att = opener.open(att_uri)
         # RFC 2183 defines the content-disposition header, if you're curious
         disp = att.headers['content-disposition'].split(';')
@@ -816,7 +846,7 @@ class BugzillaBase(object):
                 if i == 'bug_file_loc':
                     data[i] = 'http://'
                 else:
-                    raise TypeError, "required field missing or empty: '%s'" % i
+                    raise TypeError("required field missing or empty: '%s'" % i)
 
         # Sort of a chicken-and-egg problem here - check_args will save you a
         # network roundtrip if your op_sys or rep_platform is bad, but at the
@@ -825,9 +855,9 @@ class BugzillaBase(object):
         # createbug() in ipython and you've already loaded querydefaults.
         if check_args:
             if data['op_sys'] not in self.querydefaults['op_sys_list']:
-                raise ValueError, "invalid value for op_sys: %s" % data['op_sys']
+                raise ValueError("invalid value for op_sys: %s" % data['op_sys'])
             if data['rep_platform'] not in self.querydefaults['rep_platform_list']:
-                raise ValueError, "invalid value for rep_platform: %s" % data['rep_platform']
+                raise ValueError("invalid value for rep_platform: %s" % data['rep_platform'])
         # Actually perform the createbug call.
         # We return a nearly-empty Bug object, which is kind of a bummer 'cuz
         # it'll take another network roundtrip to fill it. We *could* fake it
@@ -980,9 +1010,9 @@ class CookieTransport(xmlrpclib.Transport):
         # ADDED: construct the URL and Request object for proper cookie handling
         request_url = "%s://%s%s" % (self.scheme,host,handler)
         log.debug("request_url is %s" % request_url)
-        cookie_request  = urllib2.Request(request_url)
+        cookie_request  = Request(request_url)
 
-        self.send_request(h,handler,request_body)
+        self.send_request(h,handler,request_body,verbose)
         self.send_host(h,host)
         self.send_cookies(h,cookie_request) # ADDED. creates cookiejar if None.
         self.send_user_agent(h)
@@ -999,7 +1029,7 @@ class CookieTransport(xmlrpclib.Transport):
         if hasattr(self.cookiejar,'save'):
             try:
                 self.cookiejar.save(self.cookiejar.filename)
-            except Exception, e:
+            except Exception as e:
                 log.error("Couldn't write cookiefile %s: %s" % \
                         (self.cookiejar.filename,str(e)))
 
@@ -1029,16 +1059,26 @@ class CookieTransport(xmlrpclib.Transport):
         # ADDED: construct the URL and Request object for proper cookie handling
         request_url = "%s://%s%s" % (self.scheme,host,handler)
         log.debug("request_url is %s" % request_url)
-        cookie_request  = urllib2.Request(request_url)
+        cookie_request  = Request(request_url)
 
         try:
-            self.send_request(h,handler,request_body)
-            self.send_host(h,host)
-            self.send_cookies(h,cookie_request) # ADDED. creates cookiejar if None.
-            self.send_user_agent(h)
-            self.send_content(h,request_body)
+            if six.PY3:
+                h.putrequest("POST", handler)
+                # ADDED. creates cookiejar if None.
+                self.send_cookies(h,cookie_request)
+                h.putheader('Content-Type', 'text/xml')
+                h.putheader('User-Agent', self.user_agent)
+                self.send_content(h,request_body)
+                response = h.getresponse()
+            else:
+                self.send_request(h,handler,request_body, verbose)
+                self.send_host(h,host)
+                # ADDED. creates cookiejar if None.
+                self.send_cookies(h,cookie_request)
+                self.send_user_agent(h)
+                self.send_content(h,request_body)
 
-            response = h.getresponse(buffering=True)
+                response = h.getresponse(buffering=True)
 
             # ADDED: parse headers and get cookies here
             cookie_response = CookieResponse(response.msg)
@@ -1049,7 +1089,7 @@ class CookieTransport(xmlrpclib.Transport):
             if hasattr(self.cookiejar,'save'):
                 try:
                     self.cookiejar.save(self.cookiejar.filename)
-                except Exception, e:
+                except Exception as e:
                     log.error("Couldn't write cookiefile %s: %s" % \
                             (self.cookiejar.filename,str(e)))
 
@@ -1185,7 +1225,7 @@ class _Bug(object):
             if hasattr(self,'id'):
                 self.bug_id = self.id
             else:
-                raise TypeError, "Bug object needs a bug_id"
+                raise TypeError("Bug object needs a bug_id")
 
         self.url = bugzilla.url.replace('xmlrpc.cgi',
                                         'show_bug.cgi?id=%i' % self.bug_id)
@@ -1206,7 +1246,7 @@ class _Bug(object):
         'print bug' is not recommended because of potential encoding issues.
         Please use unicode(bug) where possible.
         '''
-        return unicode(self).encode(locale.getpreferredencoding(), 'replace')
+        return six.text_type(self).encode(locale.getpreferredencoding(), 'replace')
 
     def __unicode__(self):
         '''Return a simple unicode string representation of this bug'''
@@ -1227,7 +1267,7 @@ class _Bug(object):
         if 'assigned_to' not in self.__dict__:
             if 'assigned_to_id' in self.__dict__:
                 self.assigned_to = self.bugzilla._getuserforid(self.assigned_to_id)
-        return u"#%-6s %-10s - %s - %s" % (self.bug_id,self.bug_status,
+        return six.u("#%-6s %-10s - %s - %s") % (self.bug_id,self.bug_status,
                                           self.assigned_to,desc)
     def __repr__(self):
         return '<Bug #%i on %s at %#x>' % (self.bug_id,self.bugzilla.url,
@@ -1325,7 +1365,7 @@ class _Bug(object):
         Returns [bug_id, mailresults].'''
         if not (assigned_to or reporter or qa_contact):
             # XXX is ValueError the right thing to throw here?
-            raise ValueError, "You must set one of assigned_to, reporter, or qa_contact"
+            raise ValueError("You must set one of assigned_to, reporter, or qa_contact")
         # empty fields are ignored, so it's OK to send 'em
         r = self.bugzilla._setassignee(self.bug_id,assigned_to=assigned_to,
                 reporter=reporter,qa_contact=qa_contact,comment=comment)
diff --git a/bugzilla/bugzilla3.py b/bugzilla/bugzilla3.py
index 9dbd306..33f07f0 100644
--- a/bugzilla/bugzilla3.py
+++ b/bugzilla/bugzilla3.py
@@ -10,6 +10,8 @@
 # the full text of the license.
 
 import bugzilla.base
+from six.moves import map
+from six.moves import zip
 
 class Bugzilla3(bugzilla.base.BugzillaBase):
     '''Concrete implementation of the Bugzilla protocol. This one uses the
@@ -50,7 +52,7 @@ class Bugzilla3(bugzilla.base.BugzillaBase):
                 keylist = self._getbug(bugid).keys()
                 err = None
                 break
-            except Exception, err:
+            except Exception as err:
                 pass
 
         if err:
@@ -271,10 +273,10 @@ class Bugzilla34(Bugzilla32):
             "reporter": reporter,
         }
 
-        # Strip out None elements in the dict
-        for key in query.keys():
-            if query[key] is None:
-                del(query[key])
+        to_remove = [k for k in query.keys() if query[k] is None]
+        for key in to_remove:
+            del query[key]
+
         return query
 
 
diff --git a/bugzilla/bugzilla4.py b/bugzilla/bugzilla4.py
index f902011..05c814f 100644
--- a/bugzilla/bugzilla4.py
+++ b/bugzilla/bugzilla4.py
@@ -11,6 +11,8 @@
 
 import bugzilla.base
 from bugzilla.bugzilla3 import Bugzilla36
+from six.moves import map
+from six.moves import zip
 
 class Bugzilla4(Bugzilla36):
     '''Concrete implementation of the Bugzilla protocol. This one uses the
diff --git a/bugzilla/nvlbugzilla.py b/bugzilla/nvlbugzilla.py
index ec2b74a..57f3063 100644
--- a/bugzilla/nvlbugzilla.py
+++ b/bugzilla/nvlbugzilla.py
@@ -13,10 +13,24 @@
 import bugzilla.base
 from bugzilla import Bugzilla32
 
-import urllib
-import urllib2
-import urlparse
-import cookielib
+try:
+    import urllib2
+    import urlparse
+    import urllib
+except ImportError:
+    # Python 3
+    import urllib
+    urlparse = urllib2 = urllib
+
+try:
+    import cookielib
+except ImportError:
+    try:
+        import ClientCookie as cookielib
+    except ImportError:
+        # Python 3
+        import http.cookiejar as cookielib
+
 import time
 import re
 import os
@@ -158,7 +172,7 @@ If you want cache the cookies and speedup the repeated connections, remove it or
                 self.user = c.get(obs_url, 'user')
                 self.password = c.get(obs_url, 'pass')
                 bugzilla.base.log.info("Read credentials from ~/.oscrc")
-            except NoOptionError, ne:
+            except NoOptionError as ne:
                 return
 
 
diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py
index 4233d32..c9acf8a 100644
--- a/bugzilla/rhbugzilla.py
+++ b/bugzilla/rhbugzilla.py
@@ -11,11 +11,17 @@
 
 import copy
 import logging
-import xmlrpclib
+try:
+    import xmlrpclib
+except ImportError:
+    import xmlrpc.client as xmlrpclib
 
 import bugzilla.base
 from bugzilla.bugzilla3 import Bugzilla3, Bugzilla34
 from bugzilla.bugzilla4 import Bugzilla4
+import six
+from six.moves import map
+from six.moves import zip
 
 log = logging.getLogger('bugzilla')
 
@@ -154,7 +160,7 @@ class RHBugzilla(Bugzilla4):
         add_required_fields = ('product','component','initialowner','description')
         for field in add_required_fields:
             if field not in data or not data[field]:
-                raise TypeError, "mandatory fields missing: %s" % field
+                raise TypeError("mandatory fields missing: %s" % field)
         if type(data['product']) == int:
             data['product'] = self._product_id_to_name(data['product'])
         r = self._proxy.bugzilla.addComponent(data,self.user,self.password)
@@ -163,7 +169,7 @@ class RHBugzilla(Bugzilla4):
         edit_required_fields = ('initialowner','product','component')
         for field in edit_required_fields:
             if field not in data or not data[field]:
-                raise TypeError, "mandatory field missing: %s" % field
+                raise TypeError("mandatory field missing: %s" % field)
         if type(data['product']) == int:
             data['product'] = self._product_id_to_name(data['product'])
         r = self._proxy.bugzilla.editComponent(data,self.user,self.password)
@@ -287,7 +293,7 @@ class RHBugzilla(Bugzilla4):
         data: 'assigned_to','reporter','qa_contact','comment'
         returns: [$id, $mailresults]'''
         # drop empty items
-        update = dict([(k,v) for k,v in data.iteritems() if (v and v != '')])
+        update = dict([(k,v) for k,v in six.iteritems(data) if (v and v != '')])
         return self._update_bug(id,update)
 
     def _updatedeps(self,id,blocked,dependson,action):
@@ -296,7 +302,7 @@ class RHBugzilla(Bugzilla4):
         action: 'add' or 'delete'
         '''
         if action not in ('add','delete'):
-            raise ValueError, "action must be 'add' or 'delete'"
+            raise ValueError("action must be 'add' or 'delete'")
         update={'%s_blocked' % action: blocked,
                 '%s_dependson' % action: dependson}
         self._update_bug(id,update)
@@ -324,13 +330,13 @@ class RHBugzilla(Bugzilla4):
         elif action == 'overwrite':
             r = self._getbug(id)
             if 'cc' not in r:
-                raise AttributeError, "Can't find cc list in bug %s" % str(id)
+                raise AttributeError("Can't find cc list in bug %s" % str(id))
             self._updatecc(id,r['cc'],'delete')
             self._updatecc(id,cclist,'add')
         # XXX we don't check inputs on other backend methods, maybe this
         # is more appropriate in the public method(s)
         else:
-            raise ValueError, "action must be 'add','delete', or 'overwrite'"
+            raise ValueError("action must be 'add','delete', or 'overwrite'")
 
     def _updatewhiteboard(self,id,text,which,action,comment,private):
         '''Update the whiteboard given by 'which' for the given bug.
@@ -348,8 +354,8 @@ class RHBugzilla(Bugzilla4):
         else:
             r = self._getbug(id)
             if which not in r:
-                raise ValueError, "No such whiteboard %s in bug %s" % \
-                                   (which,str(id))
+                raise ValueError("No such whiteboard %s in bug %s" % \
+                                   (which,str(id)))
             wb = r[which]
             if action == 'prepend':
                 update[which] = text+' '+wb
diff --git a/bugzilla/util.py b/bugzilla/util.py
index 1580d9c..298bb54 100644
--- a/bugzilla/util.py
+++ b/bugzilla/util.py
@@ -1,7 +1,11 @@
 #!/usr/bin/python
 
 import os
-import urlparse
+try:
+    import urlparse
+except ImportError:
+    # Python 3
+    import urllib as urlparse
 
 def url_to_query(url):
     '''Given a big huge bugzilla query URL, returns a query dict that can
@@ -27,13 +31,13 @@ def open_without_clobber(name, *args):
     orig_name = name
     while fd is None:
         try:
-            fd = os.open(name, os.O_CREAT|os.O_EXCL, 0666)
+            fd = os.open(name, os.O_CREAT|os.O_EXCL, 0o666)
         except OSError as e:
             if e.errno == os.errno.EEXIST:
                 name = "%s.%i" % (orig_name, count)
                 count += 1
             else: # raise IOError like regular open()
-                raise IOError, (e.errno, e.strerror, e.filename)
+                raise IOError(e.errno, e.strerror, e.filename)
     fobj = open(name, *args)
     if fd != fobj.fileno():
         os.close(fd)
diff --git a/tests/__init__.py b/tests/__init__.py
index c11dcf4..a08b182 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,10 +1,10 @@
+from __future__ import print_function
 
-import commands
 import difflib
 import os
 import shlex
 import sys
-import StringIO
+import six
 
 from tests.scriptimports import bugzillascript
 
@@ -35,7 +35,7 @@ def clicomm(argv, bzinstance):
     oldstdin = sys.stdin
     oldargv = sys.argv
     try:
-        out = StringIO.StringIO()
+        out = six.StringIO()
         sys.stdout = out
         sys.stderr = out
         sys.stdin = None
@@ -44,11 +44,11 @@ def clicomm(argv, bzinstance):
         ret = 0
         mainout = None
         try:
-            print " ".join(argv)
-            print
+            print(" ".join(argv))
+            print()
 
             mainout = bugzillascript.main(bzinstance)
-        except SystemExit, sys_e:
+        except SystemExit as sys_e:
             ret = sys_e.code
 
         outt = out.getvalue()
diff --git a/tests/manualtests/selftest.py b/tests/manualtests/selftest.py
index 68bb9b7..11ee187 100755
--- a/tests/manualtests/selftest.py
+++ b/tests/manualtests/selftest.py
@@ -11,6 +11,7 @@
 # the full text of the license.
 
 import sys
+from __future__ import print_function
 sys.path.insert(0, "..") # top dir
 from bugzilla import Bugzilla
 import os, glob
@@ -54,67 +55,67 @@ def encoded_filename_test():
     assert att.name.encode("utf8") == attachname
 
 def selftest(data,user='',password=''):
-    print "Using bugzilla at " + data['url']
+    print("Using bugzilla at " + data['url'])
     bz = Bugzilla(url=data['url'])
-    print "Bugzilla class: %s" % bz.__class__
+    print("Bugzilla class: %s" % bz.__class__)
     if not bz.logged_in:
         if user and password:
             bz.login(user,password)
     if bz.logged_in:
-        print "Logged in to bugzilla OK."
+        print("Logged in to bugzilla OK.")
     else:
-        print "Not logged in - create a .bugzillarc or provide user/password"
+        print("Not logged in - create a .bugzillarc or provide user/password")
         # FIXME: only run some tests if .logged_in
 
-    print "Reading product list"
+    print("Reading product list")
     prod = bz.getproducts()
     prodlist = [p['name'] for p in prod]
-    print "Products found: %s, %s, %s...(%i more)" % \
-        (prodlist[0],prodlist[1],prodlist[2],len(prodlist)-3)
+    print("Products found: %s, %s, %s...(%i more)" % \
+        (prodlist[0],prodlist[1],prodlist[2],len(prodlist)-3))
 
     p = data['query']['product']
     assert p in prodlist
-    print "Getting component list for %s" % p
+    print("Getting component list for %s" % p)
     comp = bz.getcomponents(p)
-    print "%i components found" % len(comp)
+    print("%i components found" % len(comp))
 
 
-    print "Reading public bug (#%i)" % data['public_bug']
-    print bz.getbugsimple(data['public_bug'])
-    print
+    print("Reading public bug (#%i)" % data['public_bug'])
+    print(bz.getbugsimple(data['public_bug']))
+    print()
 
-    print "Reading private bug (#%i)" % data['private_bug']
+    print("Reading private bug (#%i)" % data['private_bug'])
     try:
-        print bz.getbugsimple(data['private_bug'])
-    except xmlrpclib.Fault, e:
+        print(bz.getbugsimple(data['private_bug']))
+    except xmlrpclib.Fault as e:
         if 'NotPermitted' in e.faultString:
-            print "Failed: Not authorized."
+            print("Failed: Not authorized.")
         else:
-            print "Failed: Unknown XMLRPC error: %s"  % e
-    except Exception, e:
-        print "Failed: %s" % e
-    print
+            print("Failed: Unknown XMLRPC error: %s"  % e)
+    except Exception as e:
+        print("Failed: %s" % e)
+    print()
 
-    print "Reading multiple bugs, one-at-a-time: %s" % str(data['bugidlist'])
+    print("Reading multiple bugs, one-at-a-time: %s" % str(data['bugidlist']))
     for b in data['bugidlist']:
-        print bz.getbug(b)
-    print
+        print(bz.getbug(b))
+    print()
 
-    print "Reading multiple bugs, all-at-once: %s" % str(data['bugidlist'])
+    print("Reading multiple bugs, all-at-once: %s" % str(data['bugidlist']))
     for b in bz.getbugs(data['bugidlist']):
-        print b
-    print
+        print(b)
+    print()
 
-    print "Querying: %s" % str(data['query'])
+    print("Querying: %s" % str(data['query']))
     try:
         bugs = bz.query(data['query'])
-        print "%s bugs found." % len(bugs)
+        print("%s bugs found." % len(bugs))
         bugs = bz.getbugs([bug.bug_id for bug in bugs]) # refresh bug data
         for bug in bugs:
-            print "Bug %s" % bug
+            print("Bug %s" % bug)
     except NotImplementedError:
-        print "This bugzilla class doesn't support query()."
-    print
+        print("This bugzilla class doesn't support query().")
+    print()
 
 if __name__ == '__main__':
     user = ''
@@ -122,13 +123,13 @@ if __name__ == '__main__':
     if len(sys.argv) > 2:
         (user,password) = sys.argv[1:3]
 
-    print "Woo, welcome to the bugzilla.py self-test."
+    print("Woo, welcome to the bugzilla.py self-test.")
     for name,data in bugzillas.items():
-        print "Testing %s" % name
+        print("Testing %s" % name)
         try:
             selftest(data,user,password)
         except KeyboardInterrupt:
-            print "Exiting on keyboard interrupt."
+            print("Exiting on keyboard interrupt.")
             sys.exit(1)
     encoded_filename_test()
-    print "Awesome. We're done."
+    print("Awesome. We're done.")
-- 
1.7.10.4

