Robert Brewer avatar Robert Brewer committed a5bb72d

Freeow. The magic cherrypy/_cpcompat.py module to take all our py3k fears away.

Comments (0)

Files changed (57)

cherrypy/__init__.py

 
 __version__ = "3.2.0rc1"
 
-from urlparse import urljoin as _urljoin
-from urllib import urlencode as _urlencode
-
+from cherrypy._cpcompat import urljoin as _urljoin, urlencode as _urlencode
+from cherrypy._cpcompat import basestring, unicodestr
 
 from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect
 from cherrypy._cperror import NotFound, CherryPyException, TimeoutError
     engine.block()
 
 
-try:
-    from threading import local as _local
-except ImportError:
-    from cherrypy._cpthreadinglocal import local as _local
+from cherrypy._cpcompat import threadlocal as _local
 
 class _Serving(_local):
     """An interface for registering request and response objects.
     def __nonzero__(self):
         child = getattr(serving, self.__attrname__)
         return bool(child)
-
+    # Python 3
+    __bool__ = __nonzero__
 
 # Create request and response object (the same objects will be used
 #   throughout the entire life of the webserver, but will redirect

cherrypy/_cpchecker.py

 import warnings
 
 import cherrypy
+from cherrypy._cpcompat import iteritems, copykeys, builtins
 
 
 class Checker(object):
                 for name in dir(self):
                     if name.startswith("check_"):
                         method = getattr(self, name)
-                        if method and callable(method):
+                        if method and hasattr(method, '__call__'):
                             method()
             finally:
                 warnings.formatwarning = oldformatwarning
     
     def check_site_config_entries_in_app_config(self):
         """Check for mounted Applications that have site-scoped config."""
-        for sn, app in cherrypy.tree.apps.iteritems():
+        for sn, app in iteritems(cherrypy.tree.apps):
             if not isinstance(app, cherrypy.Application):
                 continue
             
             msg = []
-            for section, entries in app.config.iteritems():
+            for section, entries in iteritems(app.config):
                 if section.startswith('/'):
-                    for key, value in entries.iteritems():
+                    for key, value in iteritems(entries):
                         for n in ("engine.", "server.", "tree.", "checker."):
                             if key.startswith(n):
                                 msg.append("[%s] %s = %s" % (section, key, value))
     
     def _known_ns(self, app):
         ns = ["wsgi"]
-        ns.extend(app.toolboxes.keys())
-        ns.extend(app.namespaces.keys())
-        ns.extend(app.request_class.namespaces.keys())
-        ns.extend(cherrypy.config.namespaces.keys())
+        ns.extend(copykeys(app.toolboxes))
+        ns.extend(copykeys(app.namespaces))
+        ns.extend(copykeys(app.request_class.namespaces))
+        ns.extend(copykeys(cherrypy.config.namespaces))
         ns += self.extra_config_namespaces
         
         for section, conf in app.config.items():
     known_config_types = {}
     
     def _populate_known_types(self):
-        import __builtin__ as builtins
         b = [x for x in vars(builtins).values()
              if type(x) is type(str)]
         

cherrypy/_cpcompat.py

+"""Compatibility code for using CherryPy with various versions of Python.
+
+CherryPy 3.2 is compatible with Python versions 2.3+. This module provides a
+useful abstraction over the differences between Python versions, sometimes by
+preferring a newer idiom, sometimes an older one, and sometimes a custom one.
+
+In particular, Python 2 uses str and '' for byte strings, while Python 3
+uses str and '' for unicode strings. We will call each of these the 'native
+string' type for each version. Because of this major difference, this module
+provides new 'bytestr', 'unicodestr', and 'nativestr' attributes, as well as
+two functions: 'ntob', which translates native strings (of type 'str') into
+byte strings regardless of Python version, and 'ntou', which translates native
+strings to unicode strings. This also provides a 'BytesIO' name for dealing
+specifically with bytes, and a 'StringIO' name for dealing with native strings.
+It also provides a 'base64_decode' function with native strings as input and
+output.
+"""
+import os
+import sys
+
+try:
+    # Python 3
+    bytestr = bytes
+    unicodestr = str
+    nativestr = unicodestr
+    basestring = (bytes, str)
+    def ntob(n, encoding='ISO-8859-1'):
+        """Return the given native string as a byte string in the given encoding."""
+        # In Python 3, the native string type is unicode
+        return n.encode(encoding)
+    def ntou(n, encoding='ISO-8859-1'):
+        """Return the given native string as a unicode string with the given encoding."""
+        # In Python 3, the native string type is unicode
+        return n
+except NameError:
+    # Python 2
+    bytestr = str
+    unicodestr = unicode
+    nativestr = bytestr
+    basestring = basestring
+    def ntob(n, encoding='ISO-8859-1'):
+        """Return the given native string as a byte string in the given encoding."""
+        # In Python 2, the native string type is bytes. Assume it's already
+        # in the given encoding, which for ISO-8859-1 is almost always what
+        # was intended.
+        return n
+    def ntou(n, encoding='ISO-8859-1'):
+        """Return the given native string as a unicode string with the given encoding."""
+        # In Python 2, the native string type is bytes. Assume it's already
+        # in the given encoding, which for ISO-8859-1 is almost always what
+        # was intended.
+        return n.decode(encoding)
+
+try:
+    # Python 3.0+
+    # type("")
+    from io import StringIO
+    # bytes:
+    from io import BytesIO as BytesIO
+except ImportError:
+    try:
+        # type("")
+        from cStringIO import StringIO
+        # bytes:
+        BytesIO = StringIO
+    except ImportError:
+        # type("")
+        from StringIO import StringIO
+        # bytes:
+        BytesIO = StringIO
+
+try:
+    # Python 3.0+
+    from io import IOBase as FileType
+except ImportError:
+    from types import FileType
+
+try:
+    set = set
+except NameError:
+    from sets import Set as set
+
+try:
+    # Python 3.1+
+    from base64 import decodebytes as _base64_decodebytes
+except ImportError:
+    # Python 3.0-
+    # since CherryPy claims compability with Python 2.3, we must use
+    # the legacy API of base64
+    from base64 import decodestring as _base64_decodebytes
+
+def base64_decode(n, encoding='ISO-8859-1'):
+    """Return the native string base64-decoded (as a native string)."""
+    if isinstance(n, unicodestr):
+        b = n.encode(encoding)
+    else:
+        b = n
+    b = _base64_decodebytes(b)
+    if nativestr is unicodestr:
+        return b.decode(encoding)
+    else:
+        return b
+
+try:
+    # Python 2.5+
+    from hashlib import md5
+except ImportError:
+    from md5 import new as md5
+
+try:
+    # Python 2.5+
+    from hashlib import sha1 as sha
+except ImportError:
+    from sha import new as sha
+
+try:
+    sorted = sorted
+except NameError:
+    def sorted(i):
+        i = i[:]
+        i.sort()
+        return i
+
+try:
+    reversed = reversed
+except NameError:
+    def reversed(x):
+        i = len(x)
+        while i > 0:
+            i -= 1
+            yield x[i]
+
+try:
+    # Python 3
+    from urllib.parse import urljoin, urlencode
+    from urllib.parse import quote, quote_plus
+    from urllib.request import unquote, urlopen
+    from urllib.request import parse_http_list, parse_keqv_list
+except ImportError:
+    # Python 2
+    from urlparse import urljoin
+    from urllib import urlencode, urlopen
+    from urllib import quote, quote_plus
+    from urllib import unquote
+    from urllib2 import parse_http_list, parse_keqv_list
+
+try:
+    from threading import local as threadlocal
+except ImportError:
+    from cherrypy._cpthreadinglocal import local as threadlocal
+
+try:
+    dict.iteritems
+    # Python 2
+    iteritems = lambda d: d.iteritems()
+    copyitems = lambda d: d.items()
+except AttributeError:
+    # Python 3
+    iteritems = lambda d: d.items()
+    copyitems = lambda d: list(d.items())
+
+try:
+    dict.iterkeys
+    # Python 2
+    iterkeys = lambda d: d.iterkeys()
+    copykeys = lambda d: d.keys()
+except AttributeError:
+    # Python 3
+    iterkeys = lambda d: d.keys()
+    copykeys = lambda d: list(d.keys())
+
+try:
+    dict.itervalues
+    # Python 2
+    itervalues = lambda d: d.itervalues()
+    copyvalues = lambda d: d.values()
+except AttributeError:
+    # Python 3
+    itervalues = lambda d: d.values()
+    copyvalues = lambda d: list(d.values())
+
+try:
+    # Python 3
+    import builtins
+except ImportError:
+    # Python 2
+    import __builtin__ as builtins
+
+try:
+    # Python 2. We have to do it in this order so Python 2 builds
+    # don't try to import the 'http' module from cherrypy.lib
+    from Cookie import SimpleCookie, CookieError
+    from httplib import BadStatusLine, HTTPConnection, HTTPSConnection, IncompleteRead, NotConnected
+    from BaseHTTPServer import BaseHTTPRequestHandler
+except ImportError:
+    # Python 3
+    from http.cookies import SimpleCookie, CookieError
+    from http.client import BadStatusLine, HTTPConnection, HTTPSConnection, IncompleteRead, NotConnected
+    from http.server import BaseHTTPRequestHandler
+
+try:
+    # Python 2
+    xrange = xrange
+except NameError:
+    # Python 3
+    xrange = range
+
+import threading
+if hasattr(threading.Thread, "daemon"):
+    # Python 2.6+
+    def get_daemon(t):
+        return t.daemon
+    def set_daemon(t, val):
+        t.daemon = val
+else:
+    def get_daemon(t):
+        return t.isDaemon()
+    def set_daemon(t, val):
+        t.setDaemon(val)
+
+try:
+    from email.utils import formatdate
+    def HTTPDate(timeval=None):
+        return formatdate(timeval, usegmt=True)
+except ImportError:
+    from rfc822 import formatdate as HTTPDate
+
+try:
+    # Python 3
+    from urllib.parse import unquote as parse_unquote
+    def unquote_qs(atom, encoding, errors='strict'):
+        return parse_unquote(atom.replace('+', ' '), encoding=encoding, errors=errors)
+except ImportError:
+    # Python 2
+    from urllib import unquote as parse_unquote
+    def unquote_qs(atom, encoding, errors='strict'):
+        return parse_unquote(atom.replace('+', ' ')).decode(encoding, errors)
+
+try:
+    # Prefer simplejson, which is usually more advanced than the builtin module.
+    import simplejson as json
+    json_decode = json.JSONDecoder().decode
+    json_encode = json.JSONEncoder().iterencode
+except ImportError:
+    if sys.version_info >= (3, 0):
+        # Python 3.0: json is part of the standard library,
+        # but outputs unicode. We need bytes.
+        import json
+        json_decode = json.JSONDecoder().decode
+        _json_encode = json.JSONEncoder().iterencode
+        def json_encode(value):
+            for chunk in _json_encode(value):
+                yield chunk.encode('utf8')
+    elif sys.version_info >= (2, 6):
+        # Python 2.6: json is part of the standard library
+        import json
+        json_decode = json.JSONDecoder().decode
+        json_encode = json.JSONEncoder().iterencode
+    else:
+        json = None
+        def json_decode(s):
+            raise ValueError('No JSON library is available')
+        def json_encode(s):
+            raise ValueError('No JSON library is available')
+
+try:
+    import cPickle as pickle
+except ImportError:
+    # In Python 2, pickle is a Python version.
+    # In Python 3, pickle is the sped-up C version.
+    import pickle
+
+import binascii
+try:
+    os.urandom(20)
+except (AttributeError, NotImplementedError):
+    # os.urandom not available until Python 2.4. Fall back to random.random.
+    def random20():
+        return binascii.hexlify(sha(random.random())).decode('ascii')
+else:
+    def random20():
+        return binascii.hexlify(os.urandom(20)).decode('ascii')
+
+try:
+    from _thread import get_ident as get_thread_ident
+except ImportError:
+    from thread import get_ident as get_thread_ident
+
+try:
+    # Python 3
+    next = next
+except NameError:
+    # Python 2
+    def next(i):
+        return i.next()
+

cherrypy/_cpconfig.py

 style) context manager.
 """
 
-try:
-    set
-except NameError:
-    from sets import Set as set
-
 import cherrypy
+from cherrypy._cpcompat import set, basestring
 from cherrypy.lib import reprconf
 
 # Deprecated in  CherryPy 3.2--remove in 3.3

cherrypy/_cpdispatch.py

 """
 
 import string
+import sys
 import types
 
 import cherrypy
     def __call__(self):
         try:
             return self.callable(*self.args, **self.kwargs)
-        except TypeError, x:
+        except TypeError:
+            x = sys.exc_info()[1]
             try:
                 test_callable_spec(self.callable, self.args, self.kwargs)
-            except cherrypy.HTTPError, error:
-                raise error
+            except cherrypy.HTTPError:
+                raise sys.exc_info()[1]
             except:
                 raise x
             raise
             pre_len = len(iternames)
             if subnode is None:
                 dispatch = getattr(node, dispatch_name, None)
-                if dispatch and callable(dispatch) and not \
+                if dispatch and hasattr(dispatch, '__call__') and not \
                         getattr(dispatch, 'exposed', False) and \
                         pre_len > 1:
                     #Don't expose the hidden 'index' token to _cp_dispatch

cherrypy/_cperror.py

 from cgi import escape as _escape
 from sys import exc_info as _exc_info
 from traceback import format_exception as _format_exception
-from urlparse import urljoin as _urljoin
+from cherrypy._cpcompat import basestring, iteritems, urljoin as _urljoin
 from cherrypy.lib import httputil as _httputil
 
 
     if kwargs.get('version') is None:
         kwargs['version'] = cherrypy.__version__
     
-    for k, v in kwargs.iteritems():
+    for k, v in iteritems(kwargs):
         if v is None:
             kwargs[k] = ""
         else:
     error_page = pages.get(code) or pages.get('default')
     if error_page:
         try:
-            if callable(error_page):
+            if hasattr(error_page, '__call__'):
                 return error_page(**kwargs)
             else:
                 return open(error_page, 'rb').read() % kwargs
             # in one chunk or it will still get replaced! Bah.
             content = content + (" " * (s - l))
         response.body = content
-        response.headers[u'Content-Length'] = str(len(content))
+        response.headers['Content-Length'] = str(len(content))
 
 
 def format_exc(exc=None):

cherrypy/_cpmodpy.py

 """
 
 import logging
-try:
-    from cStringIO import StringIO
-except ImportError:
-    from StringIO import StringIO
+import sys
 
 import cherrypy
+from cherrypy._cpcompat import BytesIO, copyitems, ntob
 from cherrypy._cperror import format_exc, bare_error
 from cherrypy.lib import httputil
 
             path = req.uri
             qs = req.args or ""
             reqproto = req.protocol
-            headers = req.headers_in.items()
+            headers = copyitems(req.headers_in)
             rfile = _ReadOnlyRequest(req)
             prev = None
             
                     try:
                         request.run(method, path, qs, reqproto, headers, rfile)
                         break
-                    except cherrypy.InternalRedirect, ir:
+                    except cherrypy.InternalRedirect:
+                        ir = sys.exc_info()[1]
                         app.release_serving()
                         prev = request
                         
                         method = "GET"
                         path = ir.path
                         qs = ir.query_string
-                        rfile = StringIO()
+                        rfile = BytesIO()
                 
                 send_response(req, response.status, response.header_list,
                               response.body, response.stream)
 
 
 def read_process(cmd, args=""):
-    pipein, pipeout = os.popen4("%s %s" % (cmd, args))
+    fullcmd = "%s %s" % (cmd, args)
+    pipein, pipeout = os.popen4(fullcmd)
     try:
         firstline = pipeout.readline()
-        if (re.search(r"(not recognized|No such file|not found)", firstline,
+        if (re.search(ntob("(not recognized|No such file|not found)"), firstline,
                       re.IGNORECASE)):
             raise IOError('%s must be on your system path.' % cmd)
         output = firstline + pipeout.read()

cherrypy/_cpnative_server.py

 """Native adapter for serving CherryPy via its builtin server."""
 
 import logging
-try:
-    from cStringIO import StringIO
-except ImportError:
-    from StringIO import StringIO
+import sys
 
 import cherrypy
+from cherrypy._cpcompat import BytesIO
 from cherrypy._cperror import format_exc, bare_error
 from cherrypy.lib import httputil
 from cherrypy import wsgiserver
                         try:
                             request.run(method, path, qs, req.request_protocol, headers, rfile)
                             break
-                        except cherrypy.InternalRedirect, ir:
+                        except cherrypy.InternalRedirect:
+                            ir = sys.exc_info()[1]
                             app.release_serving()
                             prev = request
                             
                             method = "GET"
                             path = ir.path
                             qs = ir.query_string
-                            rfile = StringIO()
+                            rfile = BytesIO()
                     
                     self.send_response(
                         response.output_status, response.header_list,

cherrypy/_cpreqbody.py

         request = cherrypy.serving.request
         def json_processor(entity):
             \"""Read application/json data into request.json.\"""
-            if not entity.headers.get(u"Content-Length", u""):
+            if not entity.headers.get("Content-Length", ""):
                 raise cherrypy.HTTPError(411)
             
             body = entity.fp.read()
             request.body.processors.clear()
             request.body.default_proc = cherrypy.HTTPError(
                 415, 'Expected an application/json content type')
-        request.body.processors[u'application/json'] = json_processor
+        request.body.processors['application/json'] = json_processor
 
 We begin by defining a new ``json_processor`` function to stick in the ``processors``
 dictionary. All processor functions take a single argument, the ``Entity`` instance
 
 If we were defining a custom processor, we can do so without making a ``Tool``. Just add the config entry::
 
-    request.body.processors = {u'application/json': json_processor}
+    request.body.processors = {'application/json': json_processor}
 
 Note that you can only replace the ``processors`` dict wholesale this way, not update the existing one.
 """
 
 import re
+import sys
 import tempfile
 from urllib import unquote_plus
 
 import cherrypy
+from cherrypy._cpcompat import basestring, ntob, ntou
 from cherrypy.lib import httputil
 
 
     for charset in entity.attempt_charsets:
         try:
             params = {}
-            for aparam in qs.split('&'):
-                for pair in aparam.split(';'):
+            for aparam in qs.split(ntob('&')):
+                for pair in aparam.split(ntob(';')):
                     if not pair:
                         continue
                     
-                    atoms = pair.split('=', 1)
+                    atoms = pair.split(ntob('='), 1)
                     if len(atoms) == 1:
-                        atoms.append('')
+                        atoms.append(ntob(''))
                     
                     key = unquote_plus(atoms[0]).decode(charset)
                     value = unquote_plus(atoms[1]).decode(charset)
 
 def process_multipart(entity):
     """Read all multipart parts into entity.parts."""
-    ib = u""
-    if u'boundary' in entity.content_type.params:
+    ib = ""
+    if 'boundary' in entity.content_type.params:
         # http://tools.ietf.org/html/rfc2046#section-5.1.1
         # "The grammar for parameters on the Content-type field is such that it
         # is often necessary to enclose the boundary parameter values in quotes
         # on the Content-type line"
-        ib = entity.content_type.params['boundary'].strip(u'"')
+        ib = entity.content_type.params['boundary'].strip('"')
     
-    if not re.match(u"^[ -~]{0,200}[!-~]$", ib):
-        raise ValueError(u'Invalid boundary in multipart form: %r' % (ib,))
+    if not re.match("^[ -~]{0,200}[!-~]$", ib):
+        raise ValueError('Invalid boundary in multipart form: %r' % (ib,))
     
-    ib = (u'--' + ib).encode('ascii')
+    ib = ('--' + ib).encode('ascii')
     
     # Find the first marker
     while True:
     
     for part in entity.parts:
         if part.name is None:
-            key = u'parts'
+            key = ntou('parts')
         else:
             key = part.name
         
     :attr:`request.body.parts<cherrypy._cpreqbody.Entity.parts>`. You can
     enable it with::
     
-        cherrypy.request.body.processors[u'multipart'] = _cpreqbody.process_multipart
+        cherrypy.request.body.processors['multipart'] = _cpreqbody.process_multipart
     
     in an ``on_start_resource`` tool.
     """
     # "The default character set, which must be assumed in the
     # absence of a charset parameter, is US-ASCII."
     # However, many browsers send data in utf-8 with no charset.
-    attempt_charsets = [u'utf-8']
+    attempt_charsets = ['utf-8']
     """A list of strings, each of which should be a known encoding.
     
     When the Content-Type of the request body warrants it, each of the given
     given in the MIME headers for this part.
     """
     
-    default_content_type = u'application/x-www-form-urlencoded'
+    default_content_type = 'application/x-www-form-urlencoded'
     """This defines a default ``Content-Type`` to use if no Content-Type header
     is given. The empty string is used for RequestBody, which results in the
     request body not being read or parsed at all. This is by design; a missing
     the 'before_request_body' and 'before_handler' hooks (assuming that
     process_request_body is True)."""
     
-    processors = {u'application/x-www-form-urlencoded': process_urlencoded,
-                  u'multipart/form-data': process_multipart_form_data,
-                  u'multipart': process_multipart,
+    processors = {'application/x-www-form-urlencoded': process_urlencoded,
+                  'multipart/form-data': process_multipart_form_data,
+                  'multipart': process_multipart,
                   }
     """A dict of Content-Type names to processor methods."""
     
         self.parts = parts
         
         # Content-Type
-        self.content_type = headers.elements(u'Content-Type')
+        self.content_type = headers.elements('Content-Type')
         if self.content_type:
             self.content_type = self.content_type[0]
         else:
                 self.default_content_type)
         
         # Copy the class 'attempt_charsets', prepending any Content-Type charset
-        dec = self.content_type.params.get(u"charset", None)
+        dec = self.content_type.params.get("charset", None)
         if dec:
-            dec = dec.decode('ISO-8859-1')
+            #dec = dec.decode('ISO-8859-1')
             self.attempt_charsets = [dec] + [c for c in self.attempt_charsets
                                              if c != dec]
         else:
         
         # Length
         self.length = None
-        clen = headers.get(u'Content-Length', None)
+        clen = headers.get('Content-Length', None)
         # If Transfer-Encoding is 'chunked', ignore any Content-Length.
-        if clen is not None and 'chunked' not in headers.get(u'Transfer-Encoding', ''):
+        if clen is not None and 'chunked' not in headers.get('Transfer-Encoding', ''):
             try:
                 self.length = int(clen)
             except ValueError:
         # Content-Disposition
         self.name = None
         self.filename = None
-        disp = headers.elements(u'Content-Disposition')
+        disp = headers.elements('Content-Disposition')
         if disp:
             disp = disp[0]
             if 'name' in disp.params:
                 self.name = disp.params['name']
-                if self.name.startswith(u'"') and self.name.endswith(u'"'):
+                if self.name.startswith('"') and self.name.endswith('"'):
                     self.name = self.name[1:-1]
             if 'filename' in disp.params:
                 self.filename = disp.params['filename']
-                if self.filename.startswith(u'"') and self.filename.endswith(u'"'):
+                if self.filename.startswith('"') and self.filename.endswith('"'):
                     self.filename = self.filename[1:-1]
     
     # The 'type' attribute is deprecated in 3.2; remove it in 3.3.
         try:
             proc = self.processors[ct]
         except KeyError:
-            toptype = ct.split(u'/', 1)[0]
+            toptype = ct.split('/', 1)[0]
             try:
                 proc = self.processors[toptype]
             except KeyError:
     
     # "The default character set, which must be assumed in the absence of a
     # charset parameter, is US-ASCII."
-    attempt_charsets = [u'us-ascii', u'utf-8']
+    attempt_charsets = ['us-ascii', 'utf-8']
     """A list of strings, each of which should be a known encoding.
     
     When the Content-Type of the request body warrants it, each of the given
     boundary = None
     """The MIME multipart boundary."""
     
-    default_content_type = u'text/plain'
+    default_content_type = 'text/plain'
     """This defines a default ``Content-Type`` to use if no Content-Type header
     is given. The empty string is used for RequestBody, which results in the
     request body not being read or parsed at all. This is by design; a missing
             line = fp.readline()
             if not line:
                 # No more data--illegal end of headers
-                raise EOFError(u"Illegal end of headers.")
+                raise EOFError("Illegal end of headers.")
             
-            if line == '\r\n':
+            if line == ntob('\r\n'):
                 # Normal end of headers
                 break
-            if not line.endswith('\r\n'):
-                raise ValueError(u"MIME requires CRLF terminators: %r" % line)
+            if not line.endswith(ntob('\r\n')):
+                raise ValueError("MIME requires CRLF terminators: %r" % line)
             
-            if line[0] in ' \t':
+            if line[0] in ntob(' \t'):
                 # It's a continuation line.
-                v = line.strip().decode(u'ISO-8859-1')
+                v = line.strip().decode('ISO-8859-1')
             else:
-                k, v = line.split(":", 1)
-                k = k.strip().decode(u'ISO-8859-1')
-                v = v.strip().decode(u'ISO-8859-1')
+                k, v = line.split(ntob(":"), 1)
+                k = k.strip().decode('ISO-8859-1')
+                v = v.strip().decode('ISO-8859-1')
             
             existing = headers.get(k)
             if existing:
-                v = u", ".join((existing, v))
+                v = ", ".join((existing, v))
             headers[k] = v
         
         return headers
         supports the 'write' method; all bytes read will be written to the fp,
         and that fp is returned.
         """
-        endmarker = self.boundary + "--"
-        delim = ""
+        endmarker = self.boundary + ntob("--")
+        delim = ntob("")
         prev_lf = True
         lines = []
         seen = 0
         while True:
             line = self.fp.readline(1<<16)
             if not line:
-                raise EOFError(u"Illegal end of multipart body.")
-            if line.startswith("--") and prev_lf:
+                raise EOFError("Illegal end of multipart body.")
+            if line.startswith(ntob("--")) and prev_lf:
                 strippedline = line.strip()
                 if strippedline == self.boundary:
                     break
             
             line = delim + line
             
-            if line.endswith("\r\n"):
-                delim = "\r\n"
+            if line.endswith(ntob("\r\n")):
+                delim = ntob("\r\n")
                 line = line[:-2]
                 prev_lf = True
-            elif line.endswith("\n"):
-                delim = "\n"
+            elif line.endswith(ntob("\n")):
+                delim = ntob("\n")
                 line = line[:-1]
                 prev_lf = True
             else:
-                delim = ""
+                delim = ntob("")
                 prev_lf = False
             
             if fp_out is None:
                 fp_out.write(line)
         
         if fp_out is None:
-            result = ''.join(lines)
+            result = ntob('').join(lines)
             for charset in self.attempt_charsets:
                 try:
                     result = result.decode(charset)
         self.fp = fp
         self.length = length
         self.maxbytes = maxbytes
-        self.buffer = ''
+        self.buffer = ntob('')
         self.bufsize = bufsize
         self.bytes_read = 0
         self.done = False
         if remaining == 0:
             self.finish()
             if fp_out is None:
-                return ''
+                return ntob('')
             else:
                 return None
         
         if self.buffer:
             if remaining is inf:
                 data = self.buffer
-                self.buffer = ''
+                self.buffer = ntob('')
             else:
                 data = self.buffer[:remaining]
                 self.buffer = self.buffer[remaining:]
             chunksize = min(remaining, self.bufsize)
             try:
                 data = self.fp.read(chunksize)
-            except Exception, e:
+            except Exception:
+                e = sys.exc_info()[1]
                 if e.__class__.__name__ == 'MaxSizeExceeded':
                     # Post data is too big
                     raise cherrypy.HTTPError(
                 fp_out.write(data)
         
         if fp_out is None:
-            return ''.join(chunks)
+            return ntob('').join(chunks)
     
     def readline(self, size=None):
         """Read a line from the request body and return it."""
             data = self.read(chunksize)
             if not data:
                 break
-            pos = data.find('\n') + 1
+            pos = data.find(ntob('\n')) + 1
             if pos:
                 chunks.append(data[:pos])
                 remainder = data[pos:]
                 break
             else:
                 chunks.append(data)
-        return ''.join(chunks)
+        return ntob('').join(chunks)
     
     def readlines(self, sizehint=None):
         """Read lines from the request body and return them."""
             
             try:
                 for line in self.fp.read_trailer_lines():
-                    if line[0] in ' \t':
+                    if line[0] in ntob(' \t'):
                         # It's a continuation line.
                         v = line.strip()
                     else:
                         try:
-                            k, v = line.split(":", 1)
+                            k, v = line.split(ntob(":"), 1)
                         except ValueError:
                             raise ValueError("Illegal header line.")
                         k = k.strip().title()
                     if k in comma_separated_headers:
                         existing = self.trailers.get(envname)
                         if existing:
-                            v = ", ".join((existing, v))
+                            v = ntob(", ").join((existing, v))
                     self.trailers[k] = v
-            except Exception, e:
+            except Exception:
+                e = sys.exc_info()[1]
                 if e.__class__.__name__ == 'MaxSizeExceeded':
                     # Post data is too big
                     raise cherrypy.HTTPError(
     
     # Don't parse the request body at all if the client didn't provide
     # a Content-Type header. See http://www.cherrypy.org/ticket/790
-    default_content_type = u''
+    default_content_type = ''
     """This defines a default ``Content-Type`` to use if no Content-Type header
     is given. The empty string is used for RequestBody, which results in the
     request body not being read or parsed at all. This is by design; a missing
         # to have a default charset value of "ISO-8859-1" when
         # received via HTTP.
         if self.content_type.value.startswith('text/'):
-            for c in (u'ISO-8859-1', u'iso-8859-1', u'Latin-1', u'latin-1'):
+            for c in ('ISO-8859-1', 'iso-8859-1', 'Latin-1', 'latin-1'):
                 if c in self.attempt_charsets:
                     break
             else:
-                self.attempt_charsets.append(u'ISO-8859-1')
+                self.attempt_charsets.append('ISO-8859-1')
         
         # Temporary fix while deprecating passing .parts as .params.
-        self.processors[u'multipart'] = _old_process_multipart
+        self.processors['multipart'] = _old_process_multipart
         
         if request_params is None:
             request_params = {}
         # however, app developers are responsible in that case to set
         # cherrypy.request.process_body to False so this method isn't called.
         h = cherrypy.serving.request.headers
-        if u'Content-Length' not in h and u'Transfer-Encoding' not in h:
+        if 'Content-Length' not in h and 'Transfer-Encoding' not in h:
             raise cherrypy.HTTPError(411)
         
         self.fp = SizedReader(self.fp, self.length,

cherrypy/_cprequest.py

 
-from Cookie import SimpleCookie, CookieError
 import os
 import sys
 import time
-import types
 import warnings
 
 import cherrypy
+from cherrypy._cpcompat import basestring, copykeys, FileType, ntob, unicodestr
+from cherrypy._cpcompat import SimpleCookie, CookieError
 from cherrypy import _cpreqbody, _cpconfig
 from cherrypy._cperror import format_exc, bare_error
 from cherrypy.lib import httputil, file_generator
     
     def __repr__(self):
         cls = self.__class__
-        return "%s.%s(points=%r)" % (cls.__module__, cls.__name__, self.keys())
+        return "%s.%s(points=%r)" % (cls.__module__, cls.__name__, copykeys(self))
 
 
 # Config namespace handlers
                     self.stage = 'before_finalize'
                     self.hooks.run('before_finalize')
                     response.finalize()
-                except (cherrypy.HTTPRedirect, cherrypy.HTTPError), inst:
+                except (cherrypy.HTTPRedirect, cherrypy.HTTPError):
+                    inst = sys.exc_info()[1]
                     inst.set_response()
                     self.stage = 'before_finalize (HTTPError)'
                     self.hooks.run('before_finalize')
                 self.error_response()
             self.hooks.run("after_error_response")
             cherrypy.serving.response.finalize()
-        except cherrypy.HTTPRedirect, inst:
+        except cherrypy.HTTPRedirect:
+            inst = sys.exc_info()[1]
             inst.set_response()
             cherrypy.serving.response.finalize()
     
             else:
                 # [''] doesn't evaluate to False, so replace it with [].
                 value = []
-        elif isinstance(value, types.FileType):
+        elif isinstance(value, FileType):
             value = file_generator(value)
         elif value is None:
             value = []
         """Collapse self.body to a single string; replace it and return it."""
         if isinstance(self.body, basestring):
             return self.body
-
+        
         newbody = ''.join([chunk for chunk in self.body])
+        
         self.body = newbody
         return newbody
     
         """Transform headers (and cookies) into self.header_list. (Core)"""
         try:
             code, reason, _ = httputil.valid_status(self.status)
-        except ValueError, x:
-            raise cherrypy.HTTPError(500, x.args[0])
+        except ValueError:
+            raise cherrypy.HTTPError(500, sys.exc_info()[1].args[0])
         
         headers = self.headers
         
-        self.output_status = str(code) + " " + headers.encode(reason)
+        self.output_status = ntob(str(code), 'ascii') + ntob(" ") + headers.encode(reason)
         
         if self.stream:
             # The upshot: wsgiserver will chunk the response if
             # and 304 (not modified) responses MUST NOT
             # include a message-body."
             dict.pop(headers, 'Content-Length', None)
-            self.body = ""
+            self.body = ntob("")
         else:
             # Responses which are not streamed should have a Content-Length,
             # but allow user code to set Content-Length if desired.
                     # Python 2.4 emits cookies joined by LF but 2.5+ by CRLF.
                     line = line[:-1]
                 name, value = line.split(": ", 1)
-                if isinstance(name, unicode):
+                if isinstance(name, unicodestr):
                     name = name.encode("ISO-8859-1")
-                if isinstance(value, unicode):
+                if isinstance(value, unicodestr):
                     value = headers.encode(value)
                 h.append((name, value))
     

cherrypy/_cpserver.py

 
 import cherrypy
 from cherrypy.lib import attributes
+from cherrypy._cpcompat import basestring
 
 # We import * because we want to export check_port
 # et al as attributes of this module.

cherrypy/_cptools.py

 are generally either modules or instances of the tools.Tool class.
 """
 
+import sys
+import warnings
+
 import cherrypy
-import warnings
 
 
 def _getargs(func):
     """Return the names of all static arguments to the given function."""
     # Use this instead of importing inspect for less mem overhead.
     import types
-    if isinstance(func, types.MethodType):
-        func = func.im_func
-    co = func.func_code
+    if sys.version_info >= (3, 0):
+        if isinstance(func, types.MethodType):
+            func = func.__func__
+        co = func.__code__
+    else:
+        if isinstance(func, types.MethodType):
+            func = func.im_func
+        co = func.func_code
     return co.co_varnames[:co.co_argcount]
 
 

cherrypy/_cptree.py

 
 import os
 import cherrypy
+from cherrypy._cpcompat import ntou
 from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools
 from cherrypy.lib import httputil
 
         # to '' (some WSGI servers always set SCRIPT_NAME to '').
         # Try to look up the app using the full path.
         env1x = environ
-        if environ.get(u'wsgi.version') == (u'u', 0):
+        if environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
             env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ)
         path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''),
                                 env1x.get('PATH_INFO', ''))

cherrypy/_cpwsgi.py

-"""WSGI interface (see PEP 333)."""
+"""WSGI interface (see PEP 333 and 3333).
+
+Note that WSGI environ keys and values are 'native strings'; that is,
+whatever the type of "" is. For Python 2, that's a byte string; for Python 3,
+it's a unicode string. But PEP 3333 says: "even if Python's str type is
+actually Unicode "under the hood", the content of native strings must
+still be translatable to bytes via the Latin-1 encoding!"
+"""
 
 import sys as _sys
 
 import cherrypy as _cherrypy
-try:
-    from cStringIO import StringIO
-except ImportError:
-    from StringIO import StringIO
+from cherrypy._cpcompat import BytesIO
 from cherrypy import _cperror
 from cherrypy.lib import httputil
 
                 environ['REQUEST_METHOD'] = "GET"
                 environ['PATH_INFO'] = ir.path
                 environ['QUERY_STRING'] = ir.query_string
-                environ['wsgi.input'] = StringIO()
+                environ['wsgi.input'] = BytesIO()
                 environ['CONTENT_LENGTH'] = "0"
                 environ['cherrypy.previous_request'] = ir.request
 

cherrypy/lib/auth_basic.py

 __date__ = 'April 2009'
 
 import binascii
-import base64
+from cherrypy._cpcompat import base64_decode
 import cherrypy
 
 
         try:
             scheme, params = auth_header.split(' ', 1)
             if scheme.lower() == 'basic':
-                # since CherryPy claims compability with Python 2.3, we must use
-                # the legacy API of base64
-                username_password = base64.decodestring(params)
-                username, password = username_password.split(':', 1)
+                username, password = base64_decode(params).split(':', 1)
                 if checkpassword(realm, username, password):
                     if debug:
                         cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC')

cherrypy/lib/auth_digest.py

 __date__ = 'April 2009'
 
 
-try:
-    from hashlib import md5
-except ImportError:
-    # Python 2.4 and earlier
-    from md5 import new as md5
-md5_hex = lambda s: md5(s).hexdigest()
-
 import time
-import base64
-from urllib2 import parse_http_list, parse_keqv_list
+from cherrypy._cpcompat import parse_http_list, parse_keqv_list
 
 import cherrypy
+from cherrypy._cpcompat import md5, ntob
+md5_hex = lambda s: md5(ntob(s)).hexdigest()
 
 qop_auth = 'auth'
 qop_auth_int = 'auth-int'
     if auth_header is not None:
         try:
             auth = HttpDigestAuthorization(auth_header, request.method, debug=debug)
-        except ValueError, e:
-            raise cherrypy.HTTPError(400, 'Bad Request: %s' % e)
+        except ValueError:
+            raise cherrypy.HTTPError(400, "The Authorization header could not be parsed.")
         
         if debug:
             TRACE(str(auth))

cherrypy/lib/caching.py

 """
 
 import datetime
+import sys
 import threading
 import time
 
 import cherrypy
 from cherrypy.lib import cptools, httputil
+from cherrypy._cpcompat import copyitems, ntob, set_daemon, sorted
 
 
 class Cache(object):
         # Run self.expire_cache in a separate daemon thread.
         t = threading.Thread(target=self.expire_cache, name='expire_cache')
         self.expiration_thread = t
-        if hasattr(threading.Thread, "daemon"):
-            # Python 2.6+
-            t.daemon = True
-        else:
-            t.setDaemon(True)
+        set_daemon(t, True)
         t.start()
     
     def clear(self):
             now = time.time()
             # Must make a copy of expirations so it doesn't change size
             # during iteration
-            for expiration_time, objects in self.expirations.items():
+            for expiration_time, objects in copyitems(self.expirations):
                 if expiration_time <= now:
                     for obj_size, uri, sel_header_values in objects:
                         try:
         
         header_values = [request.headers.get(h, '')
                          for h in uricache.selecting_headers]
-        header_values.sort()
-        variant = uricache.wait(key=tuple(header_values),
+        variant = uricache.wait(key=tuple(sorted(header_values)),
                                 timeout=self.antistampede_timeout,
                                 debug=self.debug)
         if variant is not None:
                 # add to the cache
                 header_values = [request.headers.get(h, '')
                                  for h in uricache.selecting_headers]
-                header_values.sort()
-                uricache[tuple(header_values)] = variant
+                uricache[tuple(sorted(header_values))] = variant
                 self.tot_puts += 1
                 self.cursize = total_size
     
             # this was put into the cached copy, and should have been
             # resurrected just above (response.headers = cache_data[1]).
             cptools.validate_since()
-        except cherrypy.HTTPRedirect, x:
+        except cherrypy.HTTPRedirect:
+            x = sys.exc_info()[1]
             if x.status == 304:
                 cherrypy._cache.tot_non_modified += 1
             raise
             yield chunk
         
         # save the cache data
-        body = ''.join(output)
+        body = ntob('').join(output)
         cherrypy._cache.put((response.status, response.headers or {},
                              body, response.time), len(body))
     

cherrypy/lib/covercp.py

 import re
 import sys
 import cgi
-from urllib import quote_plus
+from cherrypy._cpcompat import quote_plus
 import os, os.path
 localFile = os.path.join(os.path.dirname(__file__), "coverage.cache")
 

cherrypy/lib/cptools.py

 """Functions for builtin CherryPy tools."""
 
 import logging
-try:
-    # Python 2.5+
-    from hashlib import md5
-except ImportError:
-    from md5 import new as md5
 import re
 
-try:
-    set
-except NameError:
-    from sets import Set as set
-
 import cherrypy
+from cherrypy._cpcompat import basestring, ntob, md5, set
 from cherrypy.lib import httputil as _httputil
 
 
         pass
     
     def login_screen(self, from_page='..', username='', error_msg='', **kwargs):
-        return """<html><body>
+        return ntob("""<html><body>
 Message: %(error_msg)s
 <form method="post" action="do_login">
     Login: <input type="text" name="username" value="%(username)s" size="10" /><br />
     <input type="submit" />
 </form>
 </body></html>""" % {'from_page': from_page, 'username': username,
-                     'error_msg': error_msg}
+                     'error_msg': error_msg}, "utf-8")
     
     def do_login(self, username, password, from_page='..', **kwargs):
         """Login. May raise redirect, or return True if request handled."""

cherrypy/lib/encoding.py

-try:
-    from cStringIO import StringIO
-except ImportError:
-    from StringIO import StringIO
-try:
-    set
-except NameError:
-    from sets import Set as set
 import struct
 import time
-import types
 
 import cherrypy
+from cherrypy._cpcompat import basestring, BytesIO, FileType, ntob, set, unicodestr
 from cherrypy.lib import file_generator
 from cherrypy.lib import set_vary_header
 
         
         def encoder(body):
             for chunk in body:
-                if isinstance(chunk, unicode):
+                if isinstance(chunk, unicodestr):
                     chunk = chunk.encode(encoding, self.errors)
                 yield chunk
         self.body = encoder(self.body)
         try:
             body = []
             for chunk in self.body:
-                if isinstance(chunk, unicode):
+                if isinstance(chunk, unicodestr):
                     chunk = chunk.encode(encoding, self.errors)
                 body.append(chunk)
             self.body = body
                 else:
                     raise cherrypy.HTTPError(500, self.failmsg % self.default_encoding)
             else:
+                for element in encs:
+                    if element.qvalue > 0:
+                        if element.value == "*":
+                            # Matches any charset. Try our default.
+                            if self.debug:
+                                cherrypy.log('Attempting default encoding due '
+                                             'to %r' % element, 'TOOLS.ENCODE')
+                            if encoder(self.default_encoding):
+                                return self.default_encoding
+                        else:
+                            encoding = element.value
+                            if self.debug:
+                                cherrypy.log('Attempting encoding %s (qvalue >'
+                                             '0)' % element, 'TOOLS.ENCODE')
+                            if encoder(encoding):
+                                return encoding
+                
                 if "*" not in charsets:
                     # If no "*" is present in an Accept-Charset field, then all
                     # character sets not explicitly mentioned get a quality
                                          'TOOLS.ENCODE')
                         if encoder(iso):
                             return iso
-                
-                for element in encs:
-                    if element.qvalue > 0:
-                        if element.value == "*":
-                            # Matches any charset. Try our default.
-                            if self.debug:
-                                cherrypy.log('Attempting default encoding due '
-                                             'to %r' % element, 'TOOLS.ENCODE')
-                            if encoder(self.default_encoding):
-                                return self.default_encoding
-                        else:
-                            encoding = element.value
-                            if self.debug:
-                                cherrypy.log('Attempting encoding %r (qvalue >'
-                                             '0)' % element, 'TOOLS.ENCODE')
-                            if encoder(encoding):
-                                return encoding
         
         # No suitable encoding found.
         ac = request.headers.get('Accept-Charset')
             else:
                 # [''] doesn't evaluate to False, so replace it with [].
                 self.body = []
-        elif isinstance(self.body, types.FileType):
+        elif isinstance(self.body, FileType):
             self.body = file_generator(self.body)
         elif self.body is None:
             self.body = []
     import zlib
     
     # See http://www.gzip.org/zlib/rfc-gzip.html
-    yield '\x1f\x8b'       # ID1 and ID2: gzip marker
-    yield '\x08'           # CM: compression method
-    yield '\x00'           # FLG: none set
+    yield ntob('\x1f\x8b')       # ID1 and ID2: gzip marker
+    yield ntob('\x08')           # CM: compression method
+    yield ntob('\x00')           # FLG: none set
     # MTIME: 4 bytes
-    yield struct.pack("<L", int(time.time()) & 0xFFFFFFFFL)
-    yield '\x02'           # XFL: max compression, slowest algo
-    yield '\xff'           # OS: unknown
+    yield struct.pack("<L", int(time.time()) & int('FFFFFFFF', 16))
+    yield ntob('\x02')           # XFL: max compression, slowest algo
+    yield ntob('\xff')           # OS: unknown
     
-    crc = zlib.crc32("")
+    crc = zlib.crc32(ntob(""))
     size = 0
     zobj = zlib.compressobj(compress_level,
                             zlib.DEFLATED, -zlib.MAX_WBITS,
     yield zobj.flush()
     
     # CRC32: 4 bytes
-    yield struct.pack("<L", crc & 0xFFFFFFFFL)
+    yield struct.pack("<L", crc & int('FFFFFFFF', 16))
     # ISIZE: 4 bytes
-    yield struct.pack("<L", size & 0xFFFFFFFFL)
+    yield struct.pack("<L", size & int('FFFFFFFF', 16))
 
 def decompress(body):
     import gzip
     
-    zbuf = StringIO()
+    zbuf = BytesIO()
     zbuf.write(body)
     zbuf.seek(0)
     zfile = gzip.GzipFile(mode='rb', fileobj=zbuf)

cherrypy/lib/httpauth.py

            "calculateNonce", "SUPPORTED_QOP")
 
 ################################################################################
-try:
-    # Python 2.5+
-    from hashlib import md5
-except ImportError:
-    from md5 import new as md5
 import time
-import base64
-from urllib2 import parse_http_list, parse_keqv_list
+from cherrypy._cpcompat import base64_decode, ntob, md5
+from cherrypy._cpcompat import parse_http_list, parse_keqv_list
 
 MD5 = "MD5"
 MD5_SESS = "MD5-sess"
 # doAuth
 #
 DIGEST_AUTH_ENCODERS = {
-    MD5: lambda val: md5(val).hexdigest(),
-    MD5_SESS: lambda val: md5(val).hexdigest(),
-#    SHA: lambda val: sha.new (val).hexdigest (),
+    MD5: lambda val: md5(ntob(val)).hexdigest(),
+    MD5_SESS: lambda val: md5(ntob(val)).hexdigest(),
+#    SHA: lambda val: sha.new(ntob(val)).hexdigest (),
 }
 
 def calculateNonce (realm, algorithm = MD5):
 
 
 def _parseBasicAuthorization (auth_params):
-    username, password = base64.decodestring (auth_params).split (":", 1)
+    username, password = base64_decode(auth_params).split(":", 1)
     return {"username": username, "password": password}
 
 AUTH_SCHEMES = {
     The 'A1' argument is only used in MD5_SESS algorithm based responses.
     Check md5SessionKey() for more info.
     """
-    global AUTH_RESPONSES
     checker = AUTH_RESPONSES[auth_map["auth_scheme"]]
     return checker (auth_map, password, method=method, encrypt=encrypt, **kwargs)
  

cherrypy/lib/httputil.py

 """
 
 from binascii import b2a_base64
-from BaseHTTPServer import BaseHTTPRequestHandler
+from cherrypy._cpcompat import BaseHTTPRequestHandler, HTTPDate, ntob, ntou, reversed, sorted
+from cherrypy._cpcompat import basestring, iteritems, unicodestr, unquote_qs
 response_codes = BaseHTTPRequestHandler.responses.copy()
 
 # From http://www.cherrypy.org/ticket/361
 import re
 import urllib
 
-from rfc822 import formatdate as HTTPDate
 
 
 def urljoin(*atoms):
     def __cmp__(self, other):
         return cmp(self.value, other.value)
     
+    def __str__(self):
+        p = [";%s=%s" % (k, v) for k, v in iteritems(self.params)]
+        return "%s%s" % (self.value, "".join(p))
+
     def __unicode__(self):
-        p = [";%s=%s" % (k, v) for k, v in self.params.iteritems()]
-        return u"%s%s" % (self.value, "".join(p))
+        return ntou(self.__str__())
     
-    def __str__(self):
-        return str(self.__unicode__())
     
     def parse(elementstr):
         """Transform 'token;key=val' to ('token', {'key': 'val'})."""
         else:
             hv = HeaderElement.from_str(element)
         result.append(hv)
-    result.sort()
-    result.reverse()
-    return result
+    
+    return list(reversed(sorted(result)))
 
 def decode_TEXT(value):
     r"""Decode :rfc:`2047` TEXT (e.g. "=?utf-8?q?f=C3=BCr?=" -> u"f\xfcr")."""
             else:
                 continue
         if len(nv[1]) or keep_blank_values:
-            name = urllib.unquote(nv[0].replace('+', ' '))
-            name = name.decode(encoding, 'strict')
-            value = urllib.unquote(nv[1].replace('+', ' '))
-            value = value.decode(encoding, 'strict')
+            name = unquote_qs(nv[0], encoding)
+            value = unquote_qs(nv[1], encoding)
             if name in d:
                 if not isinstance(d[name], list):
                     d[name] = [d[name]]
         """Transform self into a list of (name, value) tuples."""
         header_list = []
         for k, v in self.items():
-            if isinstance(k, unicode):
+            if isinstance(k, unicodestr):
                 k = self.encode(k)
             
             if not isinstance(v, basestring):
                 v = str(v)
             
-            if isinstance(v, unicode):
+            if isinstance(v, unicodestr):
                 v = self.encode(v)
             
             # See header_translate_* constants above.
             # because we never want to fold lines--folding has
             # been deprecated by the HTTP working group.
             v = b2a_base64(v.encode('utf-8'))
-            return ('=?utf-8?b?' + v.strip('\n') + '?=')
+            return (ntob('=?utf-8?b?') + v.strip(ntob('\n')) + ntob('?='))
         
         raise ValueError("Could not encode header part %r using "
                          "any of the encodings %r." %

cherrypy/lib/jsontools.py

 import sys
 import cherrypy
-
-try:
-    # Prefer simplejson, which is usually more advanced than the builtin module.
-    import simplejson as json
-except ImportError:
-    if sys.version_info >= (2, 6):
-        # Python 2.6: simplejson is part of the standard library
-        import json
-    else:
-        json = None
-
-if json is None:
-    def json_decode(s):
-        raise ValueError('No JSON library is available')
-    def json_encode(s):
-        raise ValueError('No JSON library is available')
-else:
-    json_decode = json.JSONDecoder().decode
-    json_encode = json.JSONEncoder().iterencode
+from cherrypy._cpcompat import basestring, ntou, json, json_encode, json_decode
 
 def json_processor(entity):
     """Read application/json data into request.json."""
-    if not entity.headers.get(u"Content-Length", u""):
+    if not entity.headers.get(ntou("Content-Length"), ntou("")):
         raise cherrypy.HTTPError(411)
     
     body = entity.fp.read()
     try:
-        cherrypy.serving.request.json = json_decode(body)
+        cherrypy.serving.request.json = json_decode(body.decode('utf-8'))
     except ValueError:
         raise cherrypy.HTTPError(400, 'Invalid JSON document')
 
-def json_in(content_type=[u'application/json', u'text/javascript'],
+def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
             force=True, debug=False, processor = json_processor):
     """Add a processor to parse JSON request entities:
     The default processor places the parsed data into request.json.

cherrypy/lib/profiler.py

 import sys
 import warnings
 
-try:
-    from cStringIO import StringIO
-except ImportError:
-    from StringIO import StringIO
+from cherrypy._cpcompat import BytesIO
 
 _count = 0
 
     def stats(self, filename, sortby='cumulative'):
         """:rtype stats(index): output of print_stats() for the given profile.
         """
-        sio = StringIO()
+        sio = BytesIO()
         if sys.version_info >= (2, 5):
             s = pstats.Stats(os.path.join(self.path, filename), stream=sio)
             s.strip_dirs()

cherrypy/lib/reprconf.py

 and the handler must be either a callable or a context manager.
 """
 
-from ConfigParser import ConfigParser
+try:
+    # Python 3.0+
+    from configparser import ConfigParser
+except ImportError:
+    from ConfigParser import ConfigParser
+
 try:
     set
 except NameError:

cherrypy/lib/sessions.py

 
 import datetime
 import os
-try:
-    import cPickle as pickle
-except ImportError:
-    import pickle
 import random
-try:
-    # Python 2.5+
-    from hashlib import sha1 as sha
-except ImportError:
-    from sha import new as sha
 import time
 import threading
 import types
 from warnings import warn
 
 import cherrypy
+from cherrypy._cpcompat import copyitems, pickle, random20
 from cherrypy.lib import httputil
 
 
         """Clean up expired sessions."""
         pass
     
-    try:
-        os.urandom(20)
-    except (AttributeError, NotImplementedError):
-        # os.urandom not available until Python 2.4. Fall back to random.random.
-        def generate_id(self):
-            """Return a new session id."""
-            return sha('%s' % random.random()).hexdigest()
-    else:
-        def generate_id(self):
-            """Return a new session id."""
-            return os.urandom(20).encode('hex')
+    def generate_id(self):
+        """Return a new session id."""
+        return random20()
     
     def save(self):
         """Save session data."""
     def clean_up(self):
         """Clean up expired sessions."""
         now = datetime.datetime.now()
-        for id, (data, expiration_time) in self.cache.items():
+        for id, (data, expiration_time) in copyitems(self.cache):
             if expiration_time <= now:
                 try:
                     del self.cache[id]

cherrypy/lib/static.py

 import re
 import stat
 import time
-from urllib import unquote
 
 import cherrypy
+from cherrypy._cpcompat import ntob, unquote
 from cherrypy.lib import cptools, httputil, file_generator_limited
 
 
             else:
                 # Return a multipart/byteranges response.
                 response.status = "206 Partial Content"
-                import mimetools
-                boundary = mimetools.choose_boundary()
+                from mimetools import choose_boundary
+                boundary = choose_boundary()
                 ct = "multipart/byteranges; boundary=%s" % boundary
                 response.headers['Content-Type'] = ct
                 if "Content-Length" in response.headers:
                 
                 def file_ranges():
                     # Apache compatibility:
-                    yield "\r\n"
+                    yield ntob("\r\n")
                     
                     for start, stop in r:
                         if debug:
                             cherrypy.log('Multipart; start: %r, stop: %r' % (start, stop),
                                          'TOOLS.STATIC')
-                        yield "--" + boundary
-                        yield "\r\nContent-type: %s" % content_type
-                        yield ("\r\nContent-range: bytes %s-%s/%s\r\n\r\n"
-                               % (start, stop - 1, content_length))
+                        yield ntob("--" + boundary, 'ascii')
+                        yield ntob("\r\nContent-type: %s" % content_type, 'ascii')
+                        yield ntob("\r\nContent-range: bytes %s-%s/%s\r\n\r\n"
+                                   % (start, stop - 1, content_length), 'ascii')
                         fileobj.seek(start)
                         for chunk in file_generator_limited(fileobj, stop-start):
                             yield chunk
-                        yield "\r\n"
+                        yield ntob("\r\n")
                     # Final boundary
-                    yield "--" + boundary + "--"
+                    yield ntob("--" + boundary + "--", 'ascii')
                     
                     # Apache compatibility:
-                    yield "\r\n"
+                    yield ntob("\r\n")
                 response.body = file_ranges()
             return response.body
         else:

cherrypy/process/plugins.py

 
 import os
 import re
-try:
-    set
-except NameError:
-    from sets import Set as set
 import signal as _signal
 import sys
 import time
-import thread
 import threading
 
+from cherrypy._cpcompat import basestring, get_daemon, get_thread_ident, ntob, set
+
 # _module__file__base is used by Autoreload to make
 # absolute any filenames retrieved from sys.modules which are not
 # already absolute paths.  This is to work around Python's quirk
                 # This is the first parent. Exit, now that we've forked.
                 self.bus.log('Forking once.')
                 os._exit(0)
-        except OSError, exc:
+        except OSError:
             # Python raises OSError rather than returning negative numbers.
+            exc = sys.exc_info()[1]
             sys.exit("%s: fork #1 failed: (%d) %s\n"
                      % (sys.argv[0], exc.errno, exc.strerror))
         
             if pid > 0:
                 self.bus.log('Forking twice.')
                 os._exit(0) # Exit second parent
-        except OSError, exc:
+        except OSError:
+            exc = sys.exc_info()[1]
             sys.exit("%s: fork #2 failed: (%d) %s\n"
                      % (sys.argv[0], exc.errno, exc.strerror))
         
         if self.finalized:
             self.bus.log('PID %r already written to %r.' % (pid, self.pidfile))
         else:
-            open(self.pidfile, "wb").write(str(pid))
+            open(self.pidfile, "wb").write(ntob("%s" % pid, 'utf8'))
             self.bus.log('PID %r written to %r.' % (pid, self.pidfile))
             self.finalized = True
     start.priority = 70
                 return
             try:
                 self.function(*self.args, **self.kwargs)
-            except Exception, x:
+            except Exception:
                 self.bus.log("Error in perpetual timer thread function %r." %
                              self.function, level=40, traceback=True)
                 # Quit on first error to avoid massive logs.
                 return
             try:
                 self.function(*self.args, **self.kwargs)
-            except Exception, x:
+            except Exception:
                 self.bus.log("Error in background task thread function %r." %
                              self.function, level=40, traceback=True)
                 # Quit on first error to avoid massive logs.
             if self.thread is not threading.currentThread():
                 name = self.thread.getName()
                 self.thread.cancel()
-                if hasattr(threading.Thread, "daemon"):
-                    # Python 2.6+
-                    d = self.thread.daemon
-                else:
-                    d = self.thread.isDaemon()
-                if not d:
+                if not get_daemon(self.thread):
                     self.bus.log("Joining %r" % name)
                     self.thread.join()
                 self.bus.log("Stopped thread %r." % name)
         If the current thread has already been seen, any 'start_thread'
         listeners will not be run again.
         """
-        thread_ident = thread.get_ident()
+        thread_ident = get_thread_ident()
         if thread_ident not in self.threads:
-            # We can't just use _get_ident as the thread ID
+            # We can't just use get_ident as the thread ID
             # because some platforms reuse thread ID's.
             i = len(self.threads) + 1
             self.threads[thread_ident] = i
     
     def release_thread(self):
         """Release the current thread and run 'stop_thread' listeners."""
-        thread_ident = threading._get_ident()
+        thread_ident = get_thread_ident()
         i = self.threads.pop(thread_ident, None)
         if i is not None:
             self.bus.publish('stop_thread', i)

cherrypy/process/servers.py

 of the possible configuration options.
 """
 
+import sys
 import time
 
 
         """
         try:
             self.httpserver.start()
-        except KeyboardInterrupt, exc:
+        except KeyboardInterrupt:
             self.bus.log("<Ctrl-C> hit: shutting down HTTP server")
-            self.interrupt = exc
+            self.interrupt = sys.exc_info()[1]
             self.bus.exit()
-        except SystemExit, exc:
+        except SystemExit:
             self.bus.log("SystemExit raised: shutting down HTTP server")
-            self.interrupt = exc
+            self.interrupt = sys.exc_info()[1]
             self.bus.exit()
             raise
         except:
-            import sys
             self.interrupt = sys.exc_info()[1]
             self.bus.log("Error in HTTP server: shutting down",
                          traceback=True, level=40)

cherrypy/process/wspbus.py

 
 import atexit
 import os
-try:
-    set
-except NameError:
-    from sets import Set as set
 import sys
 import threading
 import time
 import traceback as _traceback
 import warnings
 
+from cherrypy._cpcompat import set
+
 # Here I save the value of os.getcwd(), which, if I am imported early enough,
 # will be the directory from which the startup script was run.  This is needed
 # by _do_execv(), to change back to the original directory before execv()ing a

cherrypy/test/_test_decorators.py

+"""Test module for the @-decorator syntax, which is version-specific"""
+
+from cherrypy import expose, tools
+from cherrypy._cpcompat import ntob
+
+
+class ExposeExamples(object):
+    
+    @expose
+    def no_call(self):
+        return "Mr E. R. Bradshaw"
+    
+    @expose()
+    def call_empty(self):
+        return "Mrs. B.J. Smegma"
+    
+    @expose("call_alias")
+    def nesbitt(self):
+        return "Mr Nesbitt"
+    
+    @expose(["alias1", "alias2"])
+    def andrews(self):
+        return "Mr Ken Andrews"
+    
+    @expose(alias="alias3")
+    def watson(self):
+        return "Mr. and Mrs. Watson"
+
+
+class ToolExamples(object):
+    
+    @expose
+    @tools.response_headers(headers=[('Content-Type', 'application/data')])
+    def blah(self):
+        yield ntob("blah")
+    # This is here to demonstrate that _cp_config = {...} overwrites
+    # the _cp_config attribute added by the Tool decorator. You have
+    # to write _cp_config[k] = v or _cp_config.update(...) instead.
+    blah._cp_config['response.stream'] = True
+
+

cherrypy/test/benchmark.py

 import traceback
 
 import cherrypy
+from cherrypy._cpcompat import ntob
 from cherrypy import _cperror, _cpmodpy
 from cherrypy.lib import httputil
 
 
 
 class NullRequest:
-    """A null HTTP request class, returning 204 and an empty body."""
+    """A null HTTP request class, returning 200 and an empty body."""
     
     def __init__(self, local, remote, scheme="http"):
         pass
         pass
     
     def run(self, method, path, query_string, protocol, headers, rfile):
-        cherrypy.response.status = "204 No Content"
+        cherrypy.response.status = "200 OK"
         cherrypy.response.header_list = [("Content-Type", 'text/html'),