diff --git a/.gitignore b/.gitignore
index 74df2303..01a9178a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,7 @@
 *.py?
 *.egg-info
 *.swp
+.coverage
+coverage.xml
+nosetests.xml
+.tox
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 00000000..70231fa0
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,16 @@
+language: python
+python:
+  - "2.6"
+  - "2.7"
+  - "3.4"
+  - "3.5"
+  - "3.6"
+  - "3.7-dev"
+  - "nightly"
+install:
+  - pip install -r requirements.txt
+  - pip install codecov pytest-cov
+script:
+  - py.test --cov=oauth2
+after_success:
+  - codecov
diff --git a/README.md b/README.md
index 3dca1e34..67ad4222 100644
--- a/README.md
+++ b/README.md
@@ -1,357 +1,36 @@
-# Overview
-
-This code was originally forked from [Leah Culver and Andy Smith's oauth.py code](http://github.com/leah/python-oauth/). Some of the tests come from a [fork by Vic Fryzel](http://github.com/shellsage/python-oauth), while a revamped Request class and more tests were merged in from [Mark Paschal's fork](http://github.com/markpasc/python-oauth). A number of notable differences exist between this code and its forefathers:
-
-* 100% unit test coverage.
-* The <code>DataStore</code> object has been completely ripped out. While creating unit tests for the library I found several substantial bugs with the implementation and confirmed with Andy Smith that it was never fully baked.
-* Classes are no longer prefixed with <code>OAuth</code>.
-* The <code>Request</code> class now extends from <code>dict</code>.
-* The library is likely no longer compatible with Python 2.3.
-* The <code>Client</code> class works and extends from <code>httplib2</code>. It's a thin wrapper that handles automatically signing any normal HTTP request you might wish to make.
-
-# Signing a Request
-
-    import oauth2 as oauth
-    import time
-    
-    # Set the API endpoint 
-    url = "http://example.com/photos"
-    
-    # Set the base oauth_* parameters along with any other parameters required
-    # for the API call.
-    params = {
-        'oauth_version': "1.0",
-        'oauth_nonce': oauth.generate_nonce(),
-        'oauth_timestamp': int(time.time())
-        'user': 'joestump',
-        'photoid': 555555555555
-    }
-    
-    # Set up instances of our Token and Consumer. The Consumer.key and 
-    # Consumer.secret are given to you by the API provider. The Token.key and
-    # Token.secret is given to you after a three-legged authentication.
-    token = oauth.Token(key="tok-test-key", secret="tok-test-secret")
-    consumer = oauth.Consumer(key="con-test-key", secret="con-test-secret")
-    
-    # Set our token/key parameters
-    params['oauth_token'] = token.key
-    params['oauth_consumer_key'] = consumer.key
-    
-    # Create our request. Change method, etc. accordingly.
-    req = oauth.Request(method="GET", url=url, parameters=params)
-    
-    # Sign the request.
-    signature_method = oauth.SignatureMethod_HMAC_SHA1()
-    req.sign_request(signature_method, consumer, token)
-
-# Using the Client
-
-The <code>oauth2.Client</code> is based on <code>httplib2</code> and works just as you'd expect it to. The only difference is the first two arguments to the constructor are an instance of <code>oauth2.Consumer</code> and <code>oauth2.Token</code> (<code>oauth2.Token</code> is only needed for three-legged requests).
-
-    import oauth2 as oauth
-    
-    # Create your consumer with the proper key/secret.
-    consumer = oauth.Consumer(key="your-twitter-consumer-key", 
-        secret="your-twitter-consumer-secret")
-    
-    # Request token URL for Twitter.
-    request_token_url = "http://twitter.com/oauth/request_token"
-    
-    # Create our client.
-    client = oauth.Client(consumer)
-    
-    # The OAuth Client request works just like httplib2 for the most part.
-    resp, content = client.request(request_token_url, "GET")
-    print resp
-    print content
-
-# Twitter Three-legged OAuth Example
-
-Below is an example of how one would go through a three-legged OAuth flow to
-gain access to protected resources on Twitter. This is a simple CLI script, but
-can be easily translated to a web application.
-
-    import urlparse
-    import oauth2 as oauth
-    
-    consumer_key = 'my_key_from_twitter'
-    consumer_secret = 'my_secret_from_twitter'
-    
-    request_token_url = 'http://twitter.com/oauth/request_token'
-    access_token_url = 'http://twitter.com/oauth/access_token'
-    authorize_url = 'http://twitter.com/oauth/authorize'
-    
-    consumer = oauth.Consumer(consumer_key, consumer_secret)
-    client = oauth.Client(consumer)
-    
-    # Step 1: Get a request token. This is a temporary token that is used for 
-    # having the user authorize an access token and to sign the request to obtain 
-    # said access token.
-    
-    resp, content = client.request(request_token_url, "GET")
-    if resp['status'] != '200':
-        raise Exception("Invalid response %s." % resp['status'])
-    
-    request_token = dict(urlparse.parse_qsl(content))
-    
-    print "Request Token:"
-    print "    - oauth_token        = %s" % request_token['oauth_token']
-    print "    - oauth_token_secret = %s" % request_token['oauth_token_secret']
-    print 
-    
-    # Step 2: Redirect to the provider. Since this is a CLI script we do not 
-    # redirect. In a web application you would redirect the user to the URL
-    # below.
-    
-    print "Go to the following link in your browser:"
-    print "%s?oauth_token=%s" % (authorize_url, request_token['oauth_token'])
-    print 
-    
-    # After the user has granted access to you, the consumer, the provider will
-    # redirect you to whatever URL you have told them to redirect to. You can 
-    # usually define this in the oauth_callback argument as well.
-    accepted = 'n'
-    while accepted.lower() == 'n':
-        accepted = raw_input('Have you authorized me? (y/n) ')
-    oauth_verifier = raw_input('What is the PIN? ')
-    
-    # Step 3: Once the consumer has redirected the user back to the oauth_callback
-    # URL you can request the access token the user has approved. You use the 
-    # request token to sign this request. After this is done you throw away the
-    # request token and use the access token returned. You should store this 
-    # access token somewhere safe, like a database, for future use.
-    token = oauth.Token(request_token['oauth_token'],
-        request_token['oauth_token_secret'])
-    token.set_verifier(oauth_verifier)
-    client = oauth.Client(consumer, token)
-    
-    resp, content = client.request(access_token_url, "POST")
-    access_token = dict(urlparse.parse_qsl(content))
-    
-    print "Access Token:"
-    print "    - oauth_token        = %s" % access_token['oauth_token']
-    print "    - oauth_token_secret = %s" % access_token['oauth_token_secret']
-    print
-    print "You may now access protected resources using the access tokens above." 
-    print
-
-# Logging into Django w/ Twitter
-
-Twitter also has the ability to authenticate a user [via an OAuth flow](http://apiwiki.twitter.com/Sign-in-with-Twitter). This
-flow is exactly like the three-legged OAuth flow, except you send them to a 
-slightly different URL to authorize them. 
-
-In this example we'll look at how you can implement this login flow using 
-Django and python-oauth2. 
-
-## Set up a Profile model
-
-You'll need a place to store all of your Twitter OAuth credentials after the
-user has logged in. In your app's `models.py` file you should add something
-that resembles the following model.
+[![Join the chat at https://gitter.im/joestump/python-oauth2](https://img.shields.io/badge/gitter-join%20chat-1dce73.svg?style=flat-square)](https://gitter.im/joestump/python-oauth2?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](http://img.shields.io/travis-ci/joestump/python-oauth2.png?branch=master&style=flat-square)](https://travis-ci.org/joestump/python-oauth2) [![Coverage](https://img.shields.io/codecov/c/github/joestump/python-oauth2.svg?style=flat-square)](https://codecov.io/gh/joestump/python-oauth2) ![Number of issues](https://img.shields.io/github/issues/joestump/python-oauth2.svg?style=flat-square) ![Licence MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)
 
-    class Profile(models.Model):
-        user = models.ForeignKey(User)
-        oauth_token = models.CharField(max_length=200)
-        oauth_secret = models.CharField(max_length=200)
+## Note: This library implements OAuth 1.0 and *not OAuth 2.0*. 
 
-## Set up your Django views
-
-### `urls.py`
-
-Your `urls.py` should look something like the following. Basically, you need to
-have a login URL, a callback URL that Twitter will redirect your users back to,
-and a logout URL.
-
-In this example `^login/` and `twitter_login` will send the user to Twitter to
-be logged in, `^login/authenticated/` and `twitter_authenticated` will confirm
-the login, create the account if necessary, and log the user into the 
-application, and `^logout`/ logs the user out in the `twitter_logout` view.
-
-
-    from django.conf.urls.defaults import *
-    from django.contrib import admin
-    from mytwitterapp.views import twitter_login, twitter_logout, \
-        twitter_authenticated
-
-    admin.autodiscover()
-
-    urlpatterns = patterns('',
-        url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
-        url(r'^admin/', include(admin.site.urls)),
-        url(r'^login/?$', twitter_login),
-        url(r'^logout/?$', twitter_logout),
-        url(r'^login/authenticated/?$', twitter_authenticated),
-    )
-
-### `views.py`
-
-*NOTE:* The following code was coded for Python 2.4 so some of the libraries 
-and code here might need to be updated if you are using Python 2.6+. 
-
-    # Python
-    import oauth2 as oauth
-    import cgi
-
-    # Django
-    from django.shortcuts import render_to_response
-    from django.http import HttpResponseRedirect
-    from django.conf import settings
-    from django.contrib.auth import authenticate, login, logout
-    from django.contrib.auth.models import User
-    from django.contrib.auth.decorators import login_required
-
-    # Project
-    from mytwitterapp.models import Profile
-
-    # It's probably a good idea to put your consumer's OAuth token and
-    # OAuth secret into your project's settings. 
-    consumer = oauth.Consumer(settings.TWITTER_TOKEN, settings.TWITTER_SECRET)
-    client = oauth.Client(consumer)
-
-    request_token_url = 'http://twitter.com/oauth/request_token'
-    access_token_url = 'http://twitter.com/oauth/access_token'
-
-    # This is the slightly different URL used to authenticate/authorize.
-    authenticate_url = 'http://twitter.com/oauth/authenticate'
-
-    def twitter_login(request):
-        # Step 1. Get a request token from Twitter.
-        resp, content = client.request(request_token_url, "GET")
-        if resp['status'] != '200':
-            raise Exception("Invalid response from Twitter.")
-
-        # Step 2. Store the request token in a session for later use.
-        request.session['request_token'] = dict(cgi.parse_qsl(content))
+# Overview
+python-oauth2 is a python oauth library fully compatible with python versions: 2.6, 2.7, 3.3 and 3.4. This library is depended on by many other downstream packages such as Flask-Oauth.
 
-        # Step 3. Redirect the user to the authentication URL.
-        url = "%s?oauth_token=%s" % (authenticate_url,
-            request.session['request_token']['oauth_token'])
+# Installing
 
-        return HttpResponseRedirect(url)
+You can install `oauth2` via [the PIP package](https://pypi.python.org/pypi/oauth2). 
 
+    $ pip install oauth2
     
-    @login_required
-    def twitter_logout(request):
-        # Log a user out using Django's logout function and redirect them
-        # back to the homepage.
-        logout(request)
-        return HttpResponseRedirect('/')
+We recommend using [virtualenv](https://virtualenv.pypa.io/en/latest/).
 
-    def twitter_authenticated(request):
-        # Step 1. Use the request token in the session to build a new client.
-        token = oauth.Token(request.session['request_token']['oauth_token'],
-            request.session['request_token']['oauth_token_secret'])
-        client = oauth.Client(consumer, token)
-    
-        # Step 2. Request the authorized access token from Twitter.
-        resp, content = client.request(access_token_url, "GET")
-        if resp['status'] != '200':
-            print content
-            raise Exception("Invalid response from Twitter.")
-    
-        """
-        This is what you'll get back from Twitter. Note that it includes the
-        user's user_id and screen_name.
-        {
-            'oauth_token_secret': 'IcJXPiJh8be3BjDWW50uCY31chyhsMHEhqJVsphC3M',
-            'user_id': '120889797', 
-            'oauth_token': '120889797-H5zNnM3qE0iFoTTpNEHIz3noL9FKzXiOxwtnyVOD',
-            'screen_name': 'heyismysiteup'
-        }
-        """
-        access_token = dict(cgi.parse_qsl(content))
-    
-        # Step 3. Lookup the user or create them if they don't exist.
-        try:
-            user = User.objects.get(username=access_token['screen_name'])
-        except User.DoesNotExist:
-            # When creating the user I just use their screen_name@twitter.com
-            # for their email and the oauth_token_secret for their password.
-            # These two things will likely never be used. Alternatively, you 
-            # can prompt them for their email here. Either way, the password 
-            # should never be used.
-            user = User.objects.create_user(access_token['screen_name'],
-                '%s@twitter.com' % access_token['screen_name'],
-                access_token['oauth_token_secret'])
-    
-            # Save our permanent token and secret for later.
-            profile = Profile()
-            profile.user = user
-            profile.oauth_token = access_token['oauth_token']
-            profile.oauth_secret = access_token['oauth_token_secret']
-            profile.save()
-    
-        # Authenticate the user and log them in using Django's pre-built 
-        # functions for these things.
-        user = authenticate(username=access_token['screen_name'],
-            password=access_token['oauth_token_secret'])
-        login(request, user)
-    
-        return HttpResponseRedirect('/')
-    
-
-### `settings.py`
-
-* You'll likely want to set `LOGIN_URL` to `/login/` so that users are properly redirected to your Twitter login handler when you use `@login_required` in other parts of your Django app.
-* You can also set `AUTH_PROFILE_MODULE = 'mytwitterapp.Profile'` so that you can easily access the Twitter OAuth token/secret for that user using the `User.get_profile()` method in Django.
-
-# XOAUTH for IMAP and SMTP
-
-Gmail supports OAuth over IMAP and SMTP via a standard they call XOAUTH. This allows you to authenticate against Gmail's IMAP and SMTP servers using an OAuth token and secret. It also has the added benefit of allowing you to use vanilla SMTP and IMAP libraries. The `python-oauth2` package provides both IMAP and SMTP libraries that implement XOAUTH and wrap `imaplib.IMAP4_SSL` and `smtplib.SMTP`. This allows you to connect to Gmail with OAuth credentials using standard Python libraries. 
-
-## IMAP
-
-    import oauth2 as oauth
-    import oauth2.clients.imap as imaplib
-
-    # Set up your Consumer and Token as per usual. Just like any other
-    # three-legged OAuth request.
-    consumer = oauth.Consumer('your_consumer_key', 'your_consumer_secret')
-    token = oauth.Token('your_users_3_legged_token', 
-        'your_users_3_legged_token_secret')
-
-    # Setup the URL according to Google's XOAUTH implementation. Be sure
-    # to replace the email here with the appropriate email address that
-    # you wish to access.
-    url = "https://mail.google.com/mail/b/your_users_email@gmail.com/imap/"
-
-    conn = imaplib.IMAP4_SSL('imap.googlemail.com')
-    conn.debug = 4 
+# Examples
 
-    # This is the only thing in the API for impaplib.IMAP4_SSL that has 
-    # changed. You now authenticate with the URL, consumer, and token.
-    conn.authenticate(url, consumer, token)
+Examples can be found in the [wiki](https://github.com/joestump/python-oauth2/wiki)
 
-    # Once authenticated everything from the impalib.IMAP4_SSL class will 
-    # work as per usual without any modification to your code.
-    conn.select('INBOX')
-    print conn.list()
+# Running tests
+You can run tests using the following at the command line:
 
+    $ pip install -r requirements.txt
+    $ python setup.py test
 
-## SMTP
 
-    import oauth2 as oauth
-    import oauth2.clients.smtp as smtplib
-
-    # Set up your Consumer and Token as per usual. Just like any other
-    # three-legged OAuth request.
-    consumer = oauth.Consumer('your_consumer_key', 'your_consumer_secret')
-    token = oauth.Token('your_users_3_legged_token', 
-        'your_users_3_legged_token_secret')
-
-    # Setup the URL according to Google's XOAUTH implementation. Be sure
-    # to replace the email here with the appropriate email address that
-    # you wish to access.
-    url = "https://mail.google.com/mail/b/your_users_email@gmail.com/smtp/"
-
-    conn = smtplib.SMTP('smtp.googlemail.com', 587)
-    conn.set_debuglevel(True)
-    conn.ehlo('test')
-    conn.starttls()
-
-    # Again the only thing modified from smtplib.SMTP is the authenticate
-    # method, which works identically to the imaplib.IMAP4_SSL method.
-    conn.authenticate(url, consumer, token)
+# History
 
+This code was originally forked from [Leah Culver and Andy Smith's oauth.py code](http://github.com/leah/python-oauth/). Some of the tests come from a [fork by Vic Fryzel](http://github.com/shellsage/python-oauth), while a revamped Request class and more tests were merged in from [Mark Paschal's fork](http://github.com/markpasc/python-oauth). A number of notable differences exist between this code and its forefathers:
 
+* 100% unit test coverage.
+* The <code>DataStore</code> object has been completely ripped out. While creating unit tests for the library I found several substantial bugs with the implementation and confirmed with Andy Smith that it was never fully baked.
+* Classes are no longer prefixed with <code>OAuth</code>.
+* The <code>Request</code> class now extends from <code>dict</code>.
+* The library is likely no longer compatible with Python 2.3.
+* The <code>Client</code> class works and extends from <code>httplib2</code>. It's a thin wrapper that handles automatically signing any normal HTTP request you might wish to make.
diff --git a/debian/changelog b/debian/changelog
deleted file mode 100644
index 00231e10..00000000
--- a/debian/changelog
+++ /dev/null
@@ -1,60 +0,0 @@
-python-oauth2 (1.2.1) lucid; urgency=low
-
-  [ Joe Stump ]
-  * Fixed a minor spelling error, added tests for
-    oauth2.Error.__str__(), and added a test for build_xoauth_string().
-  * Added a 'make test' command to the Makefile.
-  * Added a bunch of new tests. 100% coverage has lapsed a bit. Fixing
-    now.
-
-  [ Ian Eure ]
-  * Update deb rule in makefile to sign packages and move them to
-    dist/deb.
-
-  [ Joe Stump ]
-  * Implemented a 'fix' for a bug in certain OAuth providers.
-    http://bit.ly/aUrH43
-
-  [ Ian Eure ]
-  * Update packaging.
-  * Fix version disparity.
-
-  [ coulix ]
-  * get normalised parameters should not take into account oauth_ params
-
-  [ Mathias Herberts ]
-  * Modified get_normalized_parameters so it does not encode '~' for
-    full conformance with RFC 5849 section 3.6
-
-  [ Peter Bengtsson ]
-  * made it python 2.4 compatible
-
-  [ Roderic Campbell ]
-  * tok != token. con != consumer. May be confusing for people not
-    actually reading the code.
-
-  [ Ian Eure ]
-
- -- Ian Eure <ian@simplegeo.com>  Mon, 28 Jun 2010 12:02:02 -0700
-
-python-oauth2 (1.2.1pre2) unstable; urgency=low
-
-  * Fix version disparity between setup.py & debian/changelog.
-
- -- Ian Eure <ian@simplegeo.com>  Fri, 11 Jun 2010 16:11:41 -0700
-
-python-oauth2 (1.2.1pre1) unstable; urgency=low
-
-  * Make a native package.
-  * Increment version.
-  * Make debian/rules executable.
-  * Update standards-version, maintainers.
-  * Remvoe unneeded python-central build dependency.
-
- -- Ian Eure <ian@simplegeo.com>  Fri, 11 Jun 2010 16:09:01 -0700
-
-python-oauth2 (1.0.0-1simplegeo01) karmic; urgency=low
-
-  * Initial build for SimpleGeo
-
- -- SimpleGeo Nerds <nerds@simplegeo.com>  Wed, 21 Oct 2009 23:24:00 -0700
diff --git a/debian/compat b/debian/compat
deleted file mode 100644
index 7ed6ff82..00000000
--- a/debian/compat
+++ /dev/null
@@ -1 +0,0 @@
-5
diff --git a/debian/control b/debian/control
deleted file mode 100644
index 31b2d5a4..00000000
--- a/debian/control
+++ /dev/null
@@ -1,24 +0,0 @@
-Source: python-oauth2
-Section: python
-Priority: optional
-Maintainer: SimpleGeo Nerds <nerds@simplegeo.com>
-Uploaders: Chris Lea <chris.lea@gmail.com>, Ian Eure <ian@simplegeo.com>
-Standards-Version: 3.8.4
-XS-Python-Version: all
-Build-Depends: debhelper (>= 4.1.13), cdbs (>= 0.4.49), python, python-setuptools, python-support
-Homepage: http://github.com/simplegeo/python-oauth2
-
-Package: python-oauth2
-Architecture: all
-XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, python-httplib2
-Provides: ${python:Provides}
-Suggests:
-Description: A Python OAuth class with several useful
- features.
- .
- * 100% unit test coverage.
- * The Request class now extends from dict.
- * The Client class works and extends from httplib2.
-   It's a thin wrapper that handles automatically signing
-   any normal HTTP request you might wish to make.
diff --git a/debian/copyright b/debian/copyright
deleted file mode 100644
index 8f5e8bda..00000000
--- a/debian/copyright
+++ /dev/null
@@ -1,21 +0,0 @@
-The MIT License
-
-Copyright (c) 2007 Leah Culver
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
\ No newline at end of file
diff --git a/debian/pycompat b/debian/pycompat
deleted file mode 100644
index 0cfbf088..00000000
--- a/debian/pycompat
+++ /dev/null
@@ -1 +0,0 @@
-2
diff --git a/debian/pyversions b/debian/pyversions
deleted file mode 100644
index b3dc41eb..00000000
--- a/debian/pyversions
+++ /dev/null
@@ -1 +0,0 @@
-2.5-
diff --git a/debian/rules b/debian/rules
deleted file mode 100755
index 6cccaead..00000000
--- a/debian/rules
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/make -f
-# -*- mode: makefile; coding: utf-8 -*-
-
-DEB_PYTHON_SYSTEM = pysupport
-
-include /usr/share/cdbs/1/rules/debhelper.mk
-include /usr/share/cdbs/1/class/python-distutils.mk
-
diff --git a/example/appengine_oauth.py b/example/appengine_oauth.py
index 814f9b6f..c14da764 100644
--- a/example/appengine_oauth.py
+++ b/example/appengine_oauth.py
@@ -86,7 +86,7 @@ def is_valid(self):
             request = self.get_oauth_request()
             client = self.get_client(request)
             params = self._server.verify_request(request, client, None)
-        except Exception, e:
+        except Exception as e:
             raise e
 
         return client
@@ -95,7 +95,7 @@ class SampleHandler(OAuthHandler):
     def get(self):
         try:
             client = self.is_valid()
-        except Exception, e:
+        except Exception as e:
             self.error(500)
             self.response.out.write(e)
 
diff --git a/example/client.py b/example/client.py
index 34f7dcb9..58524a9d 100644
--- a/example/client.py
+++ b/example/client.py
@@ -41,39 +41,45 @@
 CALLBACK_URL = 'http://printer.example.com/request_token_ready'
 RESOURCE_URL = 'http://photos.example.net/photos'
 
-# key and secret granted by the service provider for this consumer application - same as the MockOAuthDataStore
+# key and secret granted by the service provider for this consumer
+# application - same as the MockOAuthDataStore
 CONSUMER_KEY = 'key'
 CONSUMER_SECRET = 'secret'
 
 # example client using httplib with headers
 class SimpleOAuthClient(oauth.OAuthClient):
 
-    def __init__(self, server, port=httplib.HTTP_PORT, request_token_url='', access_token_url='', authorization_url=''):
+    def __init__(self, server, port=httplib.HTTP_PORT, request_token_url='',
+                 access_token_url='', authorization_url=''):
         self.server = server
         self.port = port
         self.request_token_url = request_token_url
         self.access_token_url = access_token_url
         self.authorization_url = authorization_url
-        self.connection = httplib.HTTPConnection("%s:%d" % (self.server, self.port))
+        self.connection = httplib.HTTPConnection(
+                            "%s:%d" % (self.server, self.port))
 
     def fetch_request_token(self, oauth_request):
         # via headers
         # -> OAuthToken
-        self.connection.request(oauth_request.http_method, self.request_token_url, headers=oauth_request.to_header()) 
+        self.connection.request(oauth_request.http_method,
+            self.request_token_url, headers=oauth_request.to_header()) 
         response = self.connection.getresponse()
         return oauth.OAuthToken.from_string(response.read())
 
     def fetch_access_token(self, oauth_request):
         # via headers
         # -> OAuthToken
-        self.connection.request(oauth_request.http_method, self.access_token_url, headers=oauth_request.to_header()) 
+        self.connection.request(oauth_request.http_method,
+            self.access_token_url, headers=oauth_request.to_header()) 
         response = self.connection.getresponse()
         return oauth.OAuthToken.from_string(response.read())
 
     def authorize_token(self, oauth_request):
         # via url
         # -> typically just some okay response
-        self.connection.request(oauth_request.http_method, oauth_request.to_url()) 
+        self.connection.request(oauth_request.http_method,
+            oauth_request.to_url()) 
         response = self.connection.getresponse()
         return response.read()
 
@@ -81,85 +87,95 @@ def access_resource(self, oauth_request):
         # via post body
         # -> some protected resources
         headers = {'Content-Type' :'application/x-www-form-urlencoded'}
-        self.connection.request('POST', RESOURCE_URL, body=oauth_request.to_postdata(), headers=headers)
+        self.connection.request('POST', RESOURCE_URL,
+                                body=oauth_request.to_postdata(),
+                                headers=headers)
         response = self.connection.getresponse()
         return response.read()
 
 def run_example():
 
     # setup
-    print '** OAuth Python Library Example **'
-    client = SimpleOAuthClient(SERVER, PORT, REQUEST_TOKEN_URL, ACCESS_TOKEN_URL, AUTHORIZATION_URL)
+    print('** OAuth Python Library Example **')
+    client = SimpleOAuthClient(SERVER, PORT, REQUEST_TOKEN_URL,
+                               ACCESS_TOKEN_URL, AUTHORIZATION_URL)
     consumer = oauth.OAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET)
     signature_method_plaintext = oauth.OAuthSignatureMethod_PLAINTEXT()
     signature_method_hmac_sha1 = oauth.OAuthSignatureMethod_HMAC_SHA1()
     pause()
 
     # get request token
-    print '* Obtain a request token ...'
+    print('* Obtain a request token ...')
     pause()
-    oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, callback=CALLBACK_URL, http_url=client.request_token_url)
+    oauth_request = oauth.OAuthRequest.from_consumer_and_token(
+        consumer, callback=CALLBACK_URL, http_url=client.request_token_url)
     oauth_request.sign_request(signature_method_plaintext, consumer, None)
-    print 'REQUEST (via headers)'
-    print 'parameters: %s' % str(oauth_request.parameters)
+    print('REQUEST (via headers)')
+    print('parameters: %s' % str(oauth_request.parameters))
     pause()
     token = client.fetch_request_token(oauth_request)
-    print 'GOT'
-    print 'key: %s' % str(token.key)
-    print 'secret: %s' % str(token.secret)
-    print 'callback confirmed? %s' % str(token.callback_confirmed)
+    print('GOT')
+    print('key: %s' % str(token.key))
+    print('secret: %s' % str(token.secret))
+    print('callback confirmed? %s' % str(token.callback_confirmed))
     pause()
 
-    print '* Authorize the request token ...'
+    print('* Authorize the request token ...')
     pause()
-    oauth_request = oauth.OAuthRequest.from_token_and_callback(token=token, http_url=client.authorization_url)
-    print 'REQUEST (via url query string)'
-    print 'parameters: %s' % str(oauth_request.parameters)
+    oauth_request = oauth.OAuthRequest.from_token_and_callback(
+        token=token, http_url=client.authorization_url)
+    print('REQUEST (via url query string)')
+    print('parameters: %s' % str(oauth_request.parameters))
     pause()
     # this will actually occur only on some callback
     response = client.authorize_token(oauth_request)
-    print 'GOT'
-    print response
+    print('GOT')
+    print(response)
     # sad way to get the verifier
     import urlparse, cgi
     query = urlparse.urlparse(response)[4]
     params = cgi.parse_qs(query, keep_blank_values=False)
     verifier = params['oauth_verifier'][0]
-    print 'verifier: %s' % verifier
+    print('verifier: %s' % verifier)
     pause()
 
     # get access token
-    print '* Obtain an access token ...'
+    print('* Obtain an access token ...')
     pause()
-    oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, verifier=verifier, http_url=client.access_token_url)
+    oauth_request = oauth.OAuthRequest.from_consumer_and_token(
+        consumer, token=token, verifier=verifier,
+        http_url=client.access_token_url)
     oauth_request.sign_request(signature_method_plaintext, consumer, token)
-    print 'REQUEST (via headers)'
-    print 'parameters: %s' % str(oauth_request.parameters)
+    print('REQUEST (via headers)')
+    print('parameters: %s' % str(oauth_request.parameters))
     pause()
     token = client.fetch_access_token(oauth_request)
-    print 'GOT'
-    print 'key: %s' % str(token.key)
-    print 'secret: %s' % str(token.secret)
+    print('GOT')
+    print('key: %s' % str(token.key))
+    print('secret: %s' % str(token.secret))
     pause()
 
     # access some protected resources
-    print '* Access protected resources ...'
+    print('* Access protected resources ...')
     pause()
-    parameters = {'file': 'vacation.jpg', 'size': 'original'} # resource specific params
-    oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, http_method='POST', http_url=RESOURCE_URL, parameters=parameters)
+    parameters = {'file': 'vacation.jpg',
+                  'size': 'original'} # resource specific params
+    oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer,
+        token=token, http_method='POST', http_url=RESOURCE_URL,
+        parameters=parameters)
     oauth_request.sign_request(signature_method_hmac_sha1, consumer, token)
-    print 'REQUEST (via post body)'
-    print 'parameters: %s' % str(oauth_request.parameters)
+    print('REQUEST (via post body)')
+    print('parameters: %s' % str(oauth_request.parameters))
     pause()
     params = client.access_resource(oauth_request)
-    print 'GOT'
-    print 'non-oauth parameters: %s' % params
+    print('GOT')
+    print('non-oauth parameters: %s' % params)
     pause()
 
 def pause():
-    print ''
+    print('')
     time.sleep(1)
 
 if __name__ == '__main__':
     run_example()
-    print 'Done.'
\ No newline at end of file
+    print('Done.')
diff --git a/example/server.py b/example/server.py
index 5986b0e2..e072f380 100644
--- a/example/server.py
+++ b/example/server.py
@@ -60,7 +60,11 @@ def lookup_token(self, token_type, token):
         return None
 
     def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
-        if oauth_token and oauth_consumer.key == self.consumer.key and (oauth_token.key == self.request_token.key or oauth_token.key == self.access_token.key) and nonce == self.nonce:
+        if (oauth_token and
+            oauth_consumer.key == self.consumer.key and
+            (oauth_token.key == self.request_token.key or
+             oauth_token.key == self.access_token.key) and
+            nonce == self.nonce):
             return self.nonce
         return None
 
@@ -74,7 +78,9 @@ def fetch_request_token(self, oauth_consumer, oauth_callback):
         return None
 
     def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
-        if oauth_consumer.key == self.consumer.key and oauth_token.key == self.request_token.key and oauth_verifier == self.verifier:
+        if (oauth_consumer.key == self.consumer.key and
+            oauth_token.key == self.request_token.key and
+            oauth_verifier == self.verifier):
             # want to check here if token is authorized
             # for mock store, we assume it is
             return self.access_token
@@ -91,8 +97,10 @@ class RequestHandler(BaseHTTPRequestHandler):
 
     def __init__(self, *args, **kwargs):
         self.oauth_server = oauth.OAuthServer(MockOAuthDataStore())
-        self.oauth_server.add_signature_method(oauth.OAuthSignatureMethod_PLAINTEXT())
-        self.oauth_server.add_signature_method(oauth.OAuthSignatureMethod_HMAC_SHA1())
+        self.oauth_server.add_signature_method(
+                    oauth.OAuthSignatureMethod_PLAINTEXT())
+        self.oauth_server.add_signature_method(
+                    oauth.OAuthSignatureMethod_HMAC_SHA1())
         BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
 
     # example way to send an oauth error
@@ -119,7 +127,8 @@ def do_GET(self):
                 pass
 
         # construct the oauth request from the request parameters
-        oauth_request = oauth.OAuthRequest.from_request(self.command, self.path, headers=self.headers, query_string=postdata)
+        oauth_request = oauth.OAuthRequest.from_request(self.command,
+            self.path, headers=self.headers, query_string=postdata)
 
         # request token
         if self.path.startswith(REQUEST_TOKEN_URL):
@@ -131,7 +140,7 @@ def do_GET(self):
                 self.end_headers()
                 # return the token
                 self.wfile.write(token.to_string())
-            except oauth.OAuthError, err:
+            except oauth.OAuthError as err:
                 self.send_oauth_error(err)
             return
 
@@ -148,7 +157,7 @@ def do_GET(self):
                 self.end_headers()
                 # return the callback url (to show server has it)
                 self.wfile.write(token.get_callback_url())
-            except oauth.OAuthError, err:
+            except oauth.OAuthError as err:
                 self.send_oauth_error(err)
             return
 
@@ -162,7 +171,7 @@ def do_GET(self):
                 self.end_headers()
                 # return the token
                 self.wfile.write(token.to_string())
-            except oauth.OAuthError, err:
+            except oauth.OAuthError as err:
                 self.send_oauth_error(err)
             return
 
@@ -170,13 +179,14 @@ def do_GET(self):
         if self.path.startswith(RESOURCE_URL):
             try:
                 # verify the request has been oauth authorized
-                consumer, token, params = self.oauth_server.verify_request(oauth_request)
+                consumer, token, params = self.oauth_server.verify_request(
+                                                    oauth_request)
                 # send okay response
                 self.send_response(200, 'OK')
                 self.end_headers()
                 # return the extra parameters - just for something to return
                 self.wfile.write(str(params))
-            except oauth.OAuthError, err:
+            except oauth.OAuthError as err:
                 self.send_oauth_error(err)
             return
 
@@ -186,10 +196,10 @@ def do_POST(self):
 def main():
     try:
         server = HTTPServer(('', 8080), RequestHandler)
-        print 'Test server running...'
+        print('Test server running...')
         server.serve_forever()
     except KeyboardInterrupt:
         server.socket.close()
 
 if __name__ == '__main__':
-    main()
\ No newline at end of file
+    main()
diff --git a/oauth2/__init__.py b/oauth2/__init__.py
index 65c3f071..38bf520b 100644
--- a/oauth2/__init__.py
+++ b/oauth2/__init__.py
@@ -22,21 +22,31 @@
 THE SOFTWARE.
 """
 
-import urllib
+import base64
+from hashlib import sha1
 import time
 import random
-import urlparse
 import hmac
 import binascii
 import httplib2
 
-try:
-    from urlparse import parse_qs, parse_qsl
-except ImportError:
-    from cgi import parse_qs, parse_qsl
-
-
-VERSION = '1.0'  # Hi Blaine!
+from ._compat import PY3
+from ._compat import b
+from ._compat import parse_qs
+from ._compat import quote
+from ._compat import STRING_TYPES
+from ._compat import TEXT
+from ._compat import u
+from ._compat import unquote
+from ._compat import unquote_to_bytes
+from ._compat import urlencode
+from ._compat import urlsplit
+from ._compat import urlunsplit
+from ._compat import urlparse
+from ._compat import urlunparse
+from ._version import __version__
+
+OAUTH_VERSION = '1.0'  # Hi Blaine!
 HTTP_METHOD = 'GET'
 SIGNATURE_METHOD = 'PLAINTEXT'
 
@@ -74,17 +84,84 @@ def build_xoauth_string(url, consumer, token=None):
     request.sign_request(signing_method, consumer, token)
 
     params = []
-    for k, v in sorted(request.iteritems()):
+    for k, v in sorted(request.items()):
         if v is not None:
             params.append('%s="%s"' % (k, escape(v)))
 
     return "%s %s %s" % ("GET", url, ','.join(params))
 
 
+def to_unicode(s):
+    """ Convert to unicode, raise exception with instructive error
+    message if s is not unicode, ascii, or utf-8. """
+    if not isinstance(s, TEXT):
+        if not isinstance(s, bytes):
+            raise TypeError('You are required to pass either unicode or '
+                            'bytes here, not: %r (%s)' % (type(s), s))
+        try:
+            s = s.decode('utf-8')
+        except UnicodeDecodeError as le:
+            raise TypeError('You are required to pass either a unicode '
+                            'object or a utf-8-enccoded bytes string here. '
+                            'You passed a bytes object which contained '
+                            'non-utf-8: %r. The UnicodeDecodeError that '
+                            'resulted from attempting to interpret it as '
+                            'utf-8 was: %s'
+                                % (s, le,))
+    return s
+
+def to_utf8(s):
+    return to_unicode(s).encode('utf-8')
+
+def to_unicode_if_string(s):
+    if isinstance(s, STRING_TYPES):
+        return to_unicode(s)
+    else:
+        return s
+
+def to_utf8_if_string(s):
+    if isinstance(s, STRING_TYPES):
+        return to_utf8(s)
+    else:
+        return s
+
+def to_unicode_optional_iterator(x):
+    """
+    Raise TypeError if x is a str containing non-utf8 bytes or if x is
+    an iterable which contains such a str.
+    """
+    if isinstance(x, STRING_TYPES):
+        return to_unicode(x)
+
+    try:
+        l = list(x)
+    except TypeError as e:
+        assert 'is not iterable' in str(e)
+        return x
+    else:
+        return [ to_unicode(e) for e in l ]
+
+def to_utf8_optional_iterator(x):
+    """
+    Raise TypeError if x is a str or if x is an iterable which
+    contains a str.
+    """
+    if isinstance(x, STRING_TYPES):
+        return to_utf8(x)
+
+    try:
+        l = list(x)
+    except TypeError as e:
+        assert 'is not iterable' in str(e)
+        return x
+    else:
+        return [ to_utf8_if_string(e) for e in l ]
+
 def escape(s):
     """Escape a URL including any /."""
-    return urllib.quote(s, safe='~')
-
+    if not isinstance(s, bytes):
+        s = s.encode('utf-8')
+    return quote(s, safe='~')
 
 def generate_timestamp():
     """Get seconds since epoch (UTC)."""
@@ -93,12 +170,12 @@ def generate_timestamp():
 
 def generate_nonce(length=8):
     """Generate pseudorandom number."""
-    return ''.join([str(random.randint(0, 9)) for i in range(length)])
+    return ''.join([str(random.SystemRandom().randint(0, 9)) for i in range(length)])
 
 
 def generate_verifier(length=8):
     """Generate pseudorandom number."""
-    return ''.join([str(random.randint(0, 9)) for i in range(length)])
+    return ''.join([str(random.SystemRandom().randint(0, 9)) for i in range(length)])
 
 
 class Consumer(object):
@@ -135,7 +212,7 @@ def __str__(self):
         data = {'oauth_consumer_key': self.key,
             'oauth_consumer_secret': self.secret}
 
-        return urllib.urlencode(data)
+        return urlencode(data)
 
 
 class Token(object):
@@ -179,13 +256,13 @@ def set_verifier(self, verifier=None):
     def get_callback_url(self):
         if self.callback and self.verifier:
             # Append the oauth_verifier.
-            parts = urlparse.urlparse(self.callback)
+            parts = urlparse(self.callback)
             scheme, netloc, path, params, query, fragment = parts[:6]
             if query:
                 query = '%s&oauth_verifier=%s' % (query, self.verifier)
             else:
                 query = 'oauth_verifier=%s' % self.verifier
-            return urlparse.urlunparse((scheme, netloc, path, params,
+            return urlunparse((scheme, netloc, path, params,
                 query, fragment))
         return self.callback
 
@@ -195,15 +272,14 @@ def to_string(self):
         The resulting string includes the token's secret, so you should never
         send or store this string where a third party can read it.
         """
-
-        data = {
-            'oauth_token': self.key,
-            'oauth_token_secret': self.secret,
-        }
+        items = [
+            ('oauth_token', self.key),
+            ('oauth_token_secret', self.secret),
+        ]
 
         if self.callback_confirmed is not None:
-            data['oauth_callback_confirmed'] = self.callback_confirmed
-        return urllib.urlencode(data)
+            items.append(('oauth_callback_confirmed', self.callback_confirmed))
+        return urlencode(items)
  
     @staticmethod
     def from_string(s):
@@ -213,7 +289,7 @@ def from_string(s):
         if not len(s):
             raise ValueError("Invalid parameter string.")
 
-        params = parse_qs(s, keep_blank_values=False)
+        params = parse_qs(u(s), keep_blank_values=False)
         if not len(params):
             raise ValueError("Invalid parameter string.")
 
@@ -266,19 +342,27 @@ class Request(dict):
  
     """
  
-    version = VERSION
- 
-    def __init__(self, method=HTTP_METHOD, url=None, parameters=None):
+    version = OAUTH_VERSION
+
+    def __init__(self, method=HTTP_METHOD, url=None, parameters=None,
+                 body=b'', is_form_encoded=False):
+        if url is not None:
+            self.url = to_unicode(url)
         self.method = method
-        self.url = url
         if parameters is not None:
-            self.update(parameters)
- 
+            for k, v in parameters.items():
+                k = to_unicode(k)
+                v = to_unicode_optional_iterator(v)
+
+                self[k] = v
+        self.body = body
+        self.is_form_encoded = is_form_encoded
+
     @setter
     def url(self, value):
         self.__dict__['url'] = value
         if value is not None:
-            scheme, netloc, path, params, query, fragment = urlparse.urlparse(value)
+            scheme, netloc, path, query, fragment = urlsplit(value)
 
             # Exclude default port numbers.
             if scheme == 'http' and netloc[-3:] == ':80':
@@ -289,7 +373,7 @@ def url(self, value):
                 raise ValueError("Unsupported URL %s (%s)." % (value, scheme))
 
             # Normalized URL excludes params, query, and fragment.
-            self.normalized_url = urlparse.urlunparse((scheme, netloc, path, None, None, None))
+            self.normalized_url = urlunsplit((scheme, netloc, path, None, None))
         else:
             self.normalized_url = None
             self.__dict__['url'] = None
@@ -303,14 +387,14 @@ def _get_timestamp_nonce(self):
  
     def get_nonoauth_parameters(self):
         """Get any non-OAuth parameters."""
-        return dict([(k, v) for k, v in self.iteritems() 
+        return dict([(k, v) for k, v in self.items() 
                     if not k.startswith('oauth_')])
  
     def to_header(self, realm=''):
         """Serialize as a header for an HTTPAuth request."""
         oauth_params = ((k, v) for k, v in self.items() 
                             if k.startswith('oauth_'))
-        stringy_params = ((k, escape(str(v))) for k, v in oauth_params)
+        stringy_params = ((k, escape(v)) for k, v in oauth_params)
         header_params = ('%s="%s"' % (k, v) for k, v in stringy_params)
         params_header = ', '.join(header_params)
  
@@ -322,40 +406,40 @@ def to_header(self, realm=''):
  
     def to_postdata(self):
         """Serialize as post data for a POST request."""
+        items = []
+        for k, v in sorted(self.items()): # predictable for testing
+            items.append((k.encode('utf-8'), to_utf8_optional_iterator(v)))
+
         # tell urlencode to deal with sequence values and map them correctly
         # to resulting querystring. for example self["k"] = ["v1", "v2"] will
         # result in 'k=v1&k=v2' and not k=%5B%27v1%27%2C+%27v2%27%5D
-        return urllib.urlencode(self, True).replace('+', '%20')
+        return urlencode(items, True).replace('+', '%20')
  
     def to_url(self):
         """Serialize as a URL for a GET request."""
-        base_url = urlparse.urlparse(self.url)
-        try:
-            query = base_url.query
-        except AttributeError:
-            # must be python <2.5
-            query = base_url[4]
-        query = parse_qs(query)
-        for k, v in self.items():
-            query.setdefault(k, []).append(v)
-        
-        try:
+        base_url = urlparse(self.url)
+
+        if PY3:
+            query = parse_qs(base_url.query)
+            for k, v in self.items():
+                query.setdefault(k, []).append(to_utf8_optional_iterator(v))
             scheme = base_url.scheme
             netloc = base_url.netloc
             path = base_url.path
             params = base_url.params
             fragment = base_url.fragment
-        except AttributeError:
-            # must be python <2.5
-            scheme = base_url[0]
-            netloc = base_url[1]
-            path = base_url[2]
-            params = base_url[3]
-            fragment = base_url[5]
-        
-        url = (scheme, netloc, path, params,
-               urllib.urlencode(query, True), fragment)
-        return urlparse.urlunparse(url)
+        else:
+            query = parse_qs(to_utf8(base_url.query))
+            for k, v in self.items():
+                query.setdefault(to_utf8(k), []).append(to_utf8_optional_iterator(v))
+            scheme = to_utf8(base_url.scheme)
+            netloc = to_utf8(base_url.netloc)
+            path = to_utf8(base_url.path)
+            params = to_utf8(base_url.params)
+            fragment = to_utf8(base_url.fragment)
+
+        url = (scheme, netloc, path, params, urlencode(query, True), fragment)
+        return urlunparse(url)
 
     def get_parameter(self, parameter):
         ret = self.get(parameter)
@@ -363,37 +447,54 @@ def get_parameter(self, parameter):
             raise Error('Parameter not found: %s' % parameter)
 
         return ret
- 
+
     def get_normalized_parameters(self):
         """Return a string that contains the parameters that must be signed."""
         items = []
-        for key, value in self.iteritems():
+        for key, value in self.items():
             if key == 'oauth_signature':
                 continue
             # 1.0a/9.1.1 states that kvp must be sorted by key, then by value,
             # so we unpack sequence values into multiple items for sorting.
-            if hasattr(value, '__iter__'):
-                items.extend((key, item) for item in value)
+            if isinstance(value, STRING_TYPES):
+                items.append((to_utf8_if_string(key), to_utf8(value)))
             else:
-                items.append((key, value))
+                try:
+                    value = list(value)
+                except TypeError as e:
+                    assert 'is not iterable' in str(e)
+                    items.append((to_utf8_if_string(key), to_utf8_if_string(value)))
+                else:
+                    items.extend((to_utf8_if_string(key), to_utf8_if_string(item)) for item in value)
 
         # Include any query string parameters from the provided URL
-        query = urlparse.urlparse(self.url)[4]
-        
+        query = urlparse(self.url)[4]
+
         url_items = self._split_url_string(query).items()
-        non_oauth_url_items = list([(k, v) for k, v in url_items  if not k.startswith('oauth_')])
-        items.extend(non_oauth_url_items)
+        url_items = [(to_utf8(k), to_utf8_optional_iterator(v)) for k, v in url_items if k != 'oauth_signature' ]
+        items.extend(url_items)
 
-        encoded_str = urllib.urlencode(sorted(items))
+        items.sort()
+        encoded_str = urlencode(items, True)
         # Encode signature parameters per Oauth Core 1.0 protocol
         # spec draft 7, section 3.6
         # (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6)
         # Spaces must be encoded with "%20" instead of "+"
         return encoded_str.replace('+', '%20').replace('%7E', '~')
- 
+
     def sign_request(self, signature_method, consumer, token):
         """Set the signature parameter to the result of sign."""
 
+        if not self.is_form_encoded:
+            # according to
+            # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
+            # section 4.1.1 "OAuth Consumers MUST NOT include an
+            # oauth_body_hash parameter on requests with form-encoded
+            # request bodies."
+            if not self.body:
+               self.body = ''
+            self['oauth_body_hash'] = base64.b64encode(sha1(to_utf8(self.body)).digest())
+
         if 'oauth_consumer_key' not in self:
             self['oauth_consumer_key'] = consumer.key
 
@@ -411,7 +512,7 @@ def make_timestamp(cls):
     @classmethod
     def make_nonce(cls):
         """Generate pseudorandom number."""
-        return str(random.randint(0, 100000000))
+        return str(random.SystemRandom().randint(0, 100000000))
  
     @classmethod
     def from_request(cls, http_method, http_url, headers=None, parameters=None,
@@ -421,10 +522,15 @@ def from_request(cls, http_method, http_url, headers=None, parameters=None,
             parameters = {}
  
         # Headers
-        if headers and 'Authorization' in headers:
-            auth_header = headers['Authorization']
+        if headers:
+            auth_header = None
+            for k, v in headers.items():
+                if k.lower() == 'authorization' or \
+                    k.upper() == 'HTTP_AUTHORIZATION':
+                    auth_header = v
+
             # Check that the authorization header is OAuth.
-            if auth_header[:6] == 'OAuth ':
+            if auth_header and auth_header[:6] == 'OAuth ':
                 auth_header = auth_header[6:]
                 try:
                     # Get the parameters from the header.
@@ -437,10 +543,11 @@ def from_request(cls, http_method, http_url, headers=None, parameters=None,
         # GET or POST query string.
         if query_string:
             query_params = cls._split_url_string(query_string)
+
             parameters.update(query_params)
  
         # URL parameters.
-        param_str = urlparse.urlparse(http_url)[4] # query
+        param_str = urlparse(http_url)[4] # query
         url_params = cls._split_url_string(param_str)
         parameters.update(url_params)
  
@@ -451,7 +558,8 @@ def from_request(cls, http_method, http_url, headers=None, parameters=None,
  
     @classmethod
     def from_consumer_and_token(cls, consumer, token=None,
-            http_method=HTTP_METHOD, http_url=None, parameters=None):
+            http_method=HTTP_METHOD, http_url=None, parameters=None,
+            body=b'', is_form_encoded=False):
         if not parameters:
             parameters = {}
  
@@ -470,7 +578,8 @@ def from_consumer_and_token(cls, consumer, token=None,
             if token.verifier:
                 parameters['oauth_verifier'] = token.verifier
  
-        return Request(http_method, http_url, parameters)
+        return cls(http_method, http_url, parameters, body=body, 
+            is_form_encoded=is_form_encoded)
  
     @classmethod
     def from_token_and_callback(cls, token, callback=None, 
@@ -500,23 +609,29 @@ def _split_header(header):
             # Split key-value.
             param_parts = param.split('=', 1)
             # Remove quotes and unescape the value.
-            params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
+            params[param_parts[0]] = unquote(param_parts[1].strip('\"'))
         return params
  
     @staticmethod
     def _split_url_string(param_str):
         """Turn URL string into parameters."""
-        parameters = parse_qs(param_str, keep_blank_values=False)
-        for k, v in parameters.iteritems():
-            parameters[k] = urllib.unquote(v[0])
+        if not PY3:
+            # If passed unicode with quoted UTF8, Python2's parse_qs leaves
+            # mojibake'd uniocde after unquoting, so encode first.
+            param_str = b(param_str, 'utf-8')
+        parameters = parse_qs(param_str, keep_blank_values=True)
+        for k, v in parameters.items():
+            if len(v) == 1:
+                parameters[k] = unquote(v[0])
+            else:
+                parameters[k] = sorted([unquote(s) for s in v])
         return parameters
 
 
 class Client(httplib2.Http):
     """OAuthClient is a worker to attempt to execute a request."""
 
-    def __init__(self, consumer, token=None, cache=None, timeout=None,
-        proxy_info=None):
+    def __init__(self, consumer, token=None, **kwargs):
 
         if consumer is not None and not isinstance(consumer, Consumer):
             raise ValueError("Invalid consumer.")
@@ -528,8 +643,7 @@ def __init__(self, consumer, token=None, cache=None, timeout=None,
         self.token = token
         self.method = SignatureMethod_HMAC_SHA1()
 
-        httplib2.Http.__init__(self, cache=cache, timeout=timeout, 
-            proxy_info=proxy_info)
+        super(Client, self).__init__(**kwargs)
 
     def set_signature_method(self, method):
         if not isinstance(method, SignatureMethod):
@@ -537,41 +651,43 @@ def set_signature_method(self, method):
 
         self.method = method
 
-    def request(self, uri, method="GET", body=None, headers=None, 
+    def request(self, uri, method="GET", body=b'', headers=None,
         redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None):
-        DEFAULT_CONTENT_TYPE = 'application/x-www-form-urlencoded'
+        DEFAULT_POST_CONTENT_TYPE = 'application/x-www-form-urlencoded'
 
         if not isinstance(headers, dict):
             headers = {}
 
-        is_multipart = method == 'POST' and headers.get('Content-Type', 
-            DEFAULT_CONTENT_TYPE) != DEFAULT_CONTENT_TYPE
+        if method == "POST":
+            headers['Content-Type'] = headers.get('Content-Type', 
+                DEFAULT_POST_CONTENT_TYPE)
+
+        is_form_encoded = \
+            headers.get('Content-Type') == 'application/x-www-form-urlencoded'
 
-        if body and method == "POST" and not is_multipart:
-            parameters = dict(parse_qsl(body))
+        if is_form_encoded and body:
+            parameters = parse_qs(body)
         else:
             parameters = None
 
         req = Request.from_consumer_and_token(self.consumer, 
             token=self.token, http_method=method, http_url=uri, 
-            parameters=parameters)
+            parameters=parameters, body=body, is_form_encoded=is_form_encoded)
 
         req.sign_request(self.method, self.consumer, self.token)
 
-        if method == "POST":
-            headers['Content-Type'] = headers.get('Content-Type', 
-                DEFAULT_CONTENT_TYPE)
-            if is_multipart:
-                headers.update(req.to_header())
-            else:
-                body = req.to_postdata()
+        scheme, netloc, path, params, query, fragment = urlparse(uri)
+        realm = urlunparse((scheme, netloc, '', None, None, None))
+
+        if is_form_encoded:
+            body = req.to_postdata()
         elif method == "GET":
             uri = req.to_url()
         else:
-            headers.update(req.to_header())
+            headers.update(req.to_header(realm=realm))
 
-        return httplib2.Http.request(self, uri, method=method, body=body, 
-            headers=headers, redirections=redirections, 
+        return httplib2.Http.request(self, uri, method=method, body=body,
+            headers=headers, redirections=redirections,
             connection_type=connection_type)
 
 
@@ -585,7 +701,7 @@ class Server(object):
     """
 
     timestamp_threshold = 300 # In seconds, five minutes.
-    version = VERSION
+    version = OAUTH_VERSION
     signature_methods = None
 
     def __init__(self, signature_methods=None):
@@ -598,7 +714,7 @@ def add_signature_method(self, signature_method):
     def verify_request(self, request, consumer, token):
         """Verifies an api call and checks all the parameters."""
 
-        version = self._get_version(request)
+        self._check_version(request)
         self._check_signature(request, consumer, token)
         parameters = request.get_nonoauth_parameters()
         return parameters
@@ -607,46 +723,46 @@ def build_authenticate_header(self, realm=''):
         """Optional support for the authenticate header."""
         return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
 
+    def _check_version(self, request):
+        """Verify the correct version of the request for this server."""
+        version = self._get_version(request)
+        if version and version != self.version:
+            raise Error('OAuth version %s not supported.' % str(version))
+
     def _get_version(self, request):
-        """Verify the correct version request for this server."""
+        """Return the version of the request for this server."""
         try:
             version = request.get_parameter('oauth_version')
         except:
-            version = VERSION
-
-        if version and version != self.version:
-            raise Error('OAuth version %s not supported.' % str(version))
+            version = OAUTH_VERSION
 
         return version
 
     def _get_signature_method(self, request):
         """Figure out the signature with some defaults."""
-        try:
-            signature_method = request.get_parameter('oauth_signature_method')
-        except:
+        signature_method = request.get('oauth_signature_method')
+        if signature_method is None:
             signature_method = SIGNATURE_METHOD
 
         try:
             # Get the signature method object.
-            signature_method = self.signature_methods[signature_method]
-        except:
+            return self.signature_methods[signature_method]
+        except KeyError:
             signature_method_names = ', '.join(self.signature_methods.keys())
-            raise Error('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
-
-        return signature_method
-
-    def _get_verifier(self, request):
-        return request.get_parameter('oauth_verifier')
+            raise Error('Signature method %s not supported try one of the '
+                        'following: %s'
+                            % (signature_method, signature_method_names))
 
     def _check_signature(self, request, consumer, token):
         timestamp, nonce = request._get_timestamp_nonce()
         self._check_timestamp(timestamp)
         signature_method = self._get_signature_method(request)
 
-        try:
-            signature = request.get_parameter('oauth_signature')
-        except:
+        signature = request.get('oauth_signature')
+        if signature is None:
             raise MissingSignature('Missing oauth_signature.')
+        if isinstance(signature, str):
+            signature = signature.encode('ascii', 'ignore')
 
         # Validate the signature.
         valid = signature_method.check(request, consumer, token, signature)
@@ -657,8 +773,6 @@ def _check_signature(self, request, consumer, token):
             raise Error('Invalid signature. Expected signature base ' 
                 'string: %s' % base)
 
-        built = signature_method.sign(request, consumer, token)
-
     def _check_timestamp(self, timestamp):
         """Verify that timestamp is recentish."""
         timestamp = int(timestamp)
@@ -679,7 +793,7 @@ class SignatureMethod(object):
     provide a new way to sign requests.
     """
 
-    def signing_base(self, request, consumer, token):
+    def signing_base(self, request, consumer, token): #pragma NO COVER
         """Calculates the string that needs to be signed.
 
         This method returns a 2-tuple containing the starting key for the
@@ -689,7 +803,7 @@ def signing_base(self, request, consumer, token):
         """
         raise NotImplementedError
 
-    def sign(self, request, consumer, token):
+    def sign(self, request, consumer, token): #pragma NO COVER
         """Returns the signature for the given request, based on the consumer
         and token also provided.
 
@@ -708,9 +822,9 @@ def check(self, request, consumer, token, signature):
 
 class SignatureMethod_HMAC_SHA1(SignatureMethod):
     name = 'HMAC-SHA1'
-        
+
     def signing_base(self, request, consumer, token):
-        if request.normalized_url is None:
+        if (not hasattr(request, 'normalized_url') or request.normalized_url is None):
             raise ValueError("Base URL for request is not set.")
 
         sig = (
@@ -723,19 +837,13 @@ def signing_base(self, request, consumer, token):
         if token:
             key += escape(token.secret)
         raw = '&'.join(sig)
-        return key, raw
+        return key.encode('ascii'), raw.encode('ascii')
 
     def sign(self, request, consumer, token):
         """Builds the base signature string."""
         key, raw = self.signing_base(request, consumer, token)
 
-        # HMAC object.
-        try:
-            from hashlib import sha1 as sha
-        except ImportError:
-            import sha # Deprecated
-
-        hashed = hmac.new(key, raw, sha)
+        hashed = hmac.new(key, raw, sha1)
 
         # Calculate the digest base 64.
         return binascii.b2a_base64(hashed.digest())[:-1]
@@ -755,4 +863,4 @@ def signing_base(self, request, consumer, token):
 
     def sign(self, request, consumer, token):
         key, raw = self.signing_base(request, consumer, token)
-        return raw
+        return raw.encode('utf8')
diff --git a/oauth2/_compat.py b/oauth2/_compat.py
new file mode 100644
index 00000000..b3f9ad01
--- /dev/null
+++ b/oauth2/_compat.py
@@ -0,0 +1,48 @@
+try:
+    TEXT = unicode
+except NameError: #pragma NO COVER Py3k
+    PY3 = True
+    TEXT = str
+    STRING_TYPES = (str, bytes)
+    def b(x, encoding='ascii'):
+        return bytes(x, encoding)
+else: #pragma NO COVER Python2
+    PY3 = False
+    STRING_TYPES = (unicode, bytes)
+    def b(x, encoding='ascii'):
+        if isinstance(x, unicode):
+            x = x.encode(encoding)
+        return x
+
+def u(x, encoding='ascii'):
+    if isinstance(x, TEXT): #pragma NO COVER
+        return x
+    try:
+        return x.decode(encoding)
+    except AttributeError: #pragma NO COVER
+        raise ValueError('WTF: %s' % x)
+
+try:
+    import urlparse
+except ImportError: #pragma NO COVER Py3k
+    from urllib.parse import parse_qs
+    from urllib.parse import parse_qsl
+    from urllib.parse import quote
+    from urllib.parse import unquote
+    from urllib.parse import unquote_to_bytes
+    from urllib.parse import urlencode
+    from urllib.parse import urlsplit
+    from urllib.parse import urlunsplit
+    from urllib.parse import urlparse
+    from urllib.parse import urlunparse
+else: #pragma NO COVER Python2
+    from urlparse import parse_qs
+    from urlparse import parse_qsl
+    from urllib import quote
+    from urllib import unquote
+    from urllib import urlencode
+    from urlparse import urlsplit
+    from urlparse import urlunsplit
+    from urlparse import urlparse
+    from urlparse import urlunparse
+    unquote_to_bytes = unquote
diff --git a/oauth2/_version.py b/oauth2/_version.py
new file mode 100644
index 00000000..3b813cb2
--- /dev/null
+++ b/oauth2/_version.py
@@ -0,0 +1,19 @@
+# This is the version of this source code.
+
+manual_verstr = "1.9"
+
+
+
+auto_build_num = "0.post1"
+
+
+
+verstr = manual_verstr + "." + auto_build_num
+try:
+    from pyutil.version_class import Version as pyutil_Version
+except (ImportError, ValueError): #pragma NO COVER
+    # Maybe there is no pyutil installed.
+    from distutils.version import LooseVersion as distutils_Version
+    __version__ = distutils_Version(verstr)
+else: #pragma NO COVER
+    __version__ = pyutil_Version(verstr)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..f65e7d36
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,4 @@
+httplib2==0.9.1
+mock==1.3.0
+pep8==1.6.2
+pytest==2.7.2
diff --git a/setup.cfg b/setup.cfg
index 07cc68ec..f0dddbe6 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,2 +1,7 @@
-[aliases]
-test = nosetests
+[nosetests]
+nocapture=1
+cover-package=oauth2
+cover-erase=1
+
+[bdist_wheel]
+universal=1
\ No newline at end of file
diff --git a/setup.py b/setup.py
index b2f9f5e5..6050f666 100755
--- a/setup.py
+++ b/setup.py
@@ -1,16 +1,54 @@
 #!/usr/bin/env python
-#from distutils.core import setup
+from __future__ import print_function
 from setuptools import setup, find_packages
+import os, re
 
-setup(name="oauth2",
-      version="1.2.1",
-      description="Library for OAuth version 1.0a.",
+PKG='oauth2'
+VERSIONFILE = os.path.join('oauth2', '_version.py')
+verstr = "unknown"
+try:
+    verstrline = open(VERSIONFILE, "rt").read()
+except EnvironmentError:
+    pass # Okay, there is no version file.
+else:
+    MVSRE = r"^manual_verstr *= *['\"]([^'\"]*)['\"]"
+    mo = re.search(MVSRE, verstrline, re.M)
+    if mo:
+        mverstr = mo.group(1)
+    else:
+        print("unable to find version in %s" % (VERSIONFILE))
+        raise RuntimeError("if %s.py exists, it must be well-formed" % (VERSIONFILE,))
+    AVSRE = r"^auto_build_num *= *['\"]([^'\"]*)['\"]"
+    mo = re.search(AVSRE, verstrline, re.M)
+    if mo:
+        averstr = mo.group(1)
+    else:
+        averstr = ''
+    verstr = '.'.join([mverstr, averstr])
+
+setup(name=PKG,
+      version=verstr,
+      description="library for OAuth version 1.9",
       author="Joe Stump",
       author_email="joe@simplegeo.com",
-      url="http://github.com/simplegeo/python-oauth2",
-      packages = find_packages(),
+      url="http://github.com/joestump/python-oauth2",
+      classifiers=[
+        "Intended Audience :: Developers",
+        "Programming Language :: Python :: 2",
+        "Programming Language :: Python :: 2.6",
+        "Programming Language :: Python :: 2.7",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.3",
+        "Programming Language :: Python :: 3.4",
+        "Programming Language :: Python :: Implementation :: CPython",
+        "Development Status :: 5 - Production/Stable",
+        "Natural Language :: English",
+        "License :: OSI Approved :: MIT License"
+      ],
+      packages = find_packages(exclude=['tests']),
       install_requires = ['httplib2'],
       license = "MIT License",
       keywords="oauth",
       zip_safe = True,
-      tests_require=['nose', 'coverage', 'mox'])
+      test_suite="tests",
+      tests_require=['mock'])
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/test_oauth.py b/tests/test_oauth.py
index e2a87f97..b9802cbc 100644
--- a/tests/test_oauth.py
+++ b/tests/test_oauth.py
@@ -1,3 +1,5 @@
+﻿# -*- coding: utf-8 -*-
+
 """
 The MIT License
 
@@ -22,45 +24,55 @@
 THE SOFTWARE.
 """
 import sys
-import os
-import unittest
-import oauth2 as oauth
 import random
 import time
-import urllib
-import urlparse
-from types import ListType
-import mox
+import unittest
+
 import httplib2
+import mock
 
-# Fix for python2.5 compatibility
-try:
-    from urlparse import parse_qs, parse_qsl
-except ImportError:
-    from cgi import parse_qs, parse_qsl
+from oauth2._compat import b
+from oauth2._compat import unquote
+from oauth2._compat import urlencode
+from oauth2._compat import urlparse
+from oauth2._compat import urlunparse
+from oauth2._compat import parse_qs
+from oauth2._compat import parse_qsl
+from oauth2._compat import u
 
+import oauth2 as oauth
 
-sys.path[0:0] = [os.path.join(os.path.dirname(__file__), ".."),]
+_UEMPTY = u('')
+_UBLANK = u(' ')
+_BSMILEY = b':-)'
+_USMILEY = u(_BSMILEY)
+_BGLYPH = b'\xae'
+_UGLYPH = u(_BGLYPH, 'latin1')
+_B2019 = b'\xe2\x80\x99' # u'\u2019' encoded to UTF-8
+_U2019 = u(_B2019, 'utf8') # u'\u2019'
+_B2766 = b'\xe2\x9d\xa6' # u'\u2766' encoded to UTF-8
+_U2766 = u(_B2766, 'utf8') # u'\u2766'
 
+PY3 = sys.version_info >= (3,)
 
 class TestError(unittest.TestCase):
     def test_message(self):
         try:
             raise oauth.Error
-        except oauth.Error, e:
+        except oauth.Error as e:
             self.assertEqual(e.message, 'OAuth error occurred.')
 
         msg = 'OMG THINGS BROKE!!!!'
         try:
             raise oauth.Error(msg)
-        except oauth.Error, e:
+        except oauth.Error as e:
             self.assertEqual(e.message, msg)
 
     def test_str(self):
         try:
             raise oauth.Error
-        except oauth.Error, e:
-            self.assertEquals(str(e), 'OAuth error occurred.')
+        except oauth.Error as e:
+            self.assertEqual(str(e), 'OAuth error occurred.')
 
 class TestGenerateFunctions(unittest.TestCase):
     def test_build_auth_header(self):
@@ -88,18 +100,18 @@ def test_build_xoauth_string(self):
         parts = oauth_string.split(',')
         for part in parts:
             var, val = part.split('=')
-            returned[var] = val.strip('"') 
+            returned[var] = val.strip('"')
 
-        self.assertEquals('HMAC-SHA1', returned['oauth_signature_method'])
-        self.assertEquals('user_token', returned['oauth_token'])
-        self.assertEquals('consumer_token', returned['oauth_consumer_key'])
+        self.assertEqual('HMAC-SHA1', returned['oauth_signature_method'])
+        self.assertEqual('user_token', returned['oauth_token'])
+        self.assertEqual('consumer_token', returned['oauth_consumer_key'])
         self.assertTrue('oauth_signature' in returned, 'oauth_signature')
 
     def test_escape(self):
         string = 'http://whatever.com/~someuser/?test=test&other=other'
-        self.assert_('~' in oauth.escape(string))
+        self.assertTrue('~' in oauth.escape(string))
         string = '../../../../../../../etc/passwd'
-        self.assert_('../' not in oauth.escape(string))
+        self.assertTrue('../' not in oauth.escape(string))
 
     def test_gen_nonce(self):
         nonce = oauth.generate_nonce()
@@ -137,8 +149,8 @@ def test_str(self):
         res = dict(parse_qsl(str(self.consumer)))
         self.assertTrue('oauth_consumer_key' in res)
         self.assertTrue('oauth_consumer_secret' in res)
-        self.assertEquals(res['oauth_consumer_key'], self.consumer.key)
-        self.assertEquals(res['oauth_consumer_secret'], self.consumer.secret)
+        self.assertEqual(res['oauth_consumer_key'], self.consumer.key)
+        self.assertEqual(res['oauth_consumer_secret'], self.consumer.secret)
 
 class TestToken(unittest.TestCase):
     def setUp(self):
@@ -205,8 +217,7 @@ def test_get_callback_url(self):
         self.assertEqual(url, '%s%s' % (cb, verifier_str))
 
     def test_to_string(self):
-        string = 'oauth_token_secret=%s&oauth_token=%s' % (self.secret,
-                                                           self.key)
+        string = 'oauth_token=%s&oauth_token_secret=%s' % (self.key, self.secret)
         self.assertEqual(self.token.to_string(), string)
 
         self.token.set_callback('http://www.example.com/my-callback')
@@ -223,9 +234,9 @@ def _compare_tokens(self, new):
         # TODO: What about copying the verifier to the new token?
         # self.assertEqual(self.token.verifier, new.verifier)
 
-    def test_to_string(self):
+    def test___str__(self):
         tok = oauth.Token('tooken', 'seecret')
-        self.assertEqual(str(tok), 'oauth_token_secret=seecret&oauth_token=tooken')
+        self.assertEqual(str(tok), 'oauth_token=tooken&oauth_token_secret=seecret')
 
     def test_from_string(self):
         self.assertRaises(ValueError, lambda: oauth.Token.from_string(''))
@@ -248,27 +259,95 @@ def test_from_string(self):
         new = oauth.Token.from_string(string)
         self._compare_tokens(new)
 
-class TestRequest(unittest.TestCase):
+class ReallyEqualMixin:
+    def assertReallyEqual(self, a, b, msg=None):
+        self.assertEqual(a, b, msg=msg)
+        self.assertEqual(type(a), type(b), msg="a :: %r, b :: %r, %r" % (a, b, msg))
+
+class TestFuncs(unittest.TestCase):
+    def test_to_unicode(self):
+        self.assertRaises(TypeError, oauth.to_unicode, 0)
+        self.assertRaises(TypeError, oauth.to_unicode, b'\xae')
+        self.assertRaises(TypeError, oauth.to_unicode_optional_iterator, b'\xae')
+        self.assertRaises(TypeError, oauth.to_unicode_optional_iterator, [b'\xae'])
+
+        self.assertEqual(oauth.to_unicode(_BSMILEY), _USMILEY)
+        self.assertEqual(oauth.to_unicode(_UGLYPH), _UGLYPH)
+        self.assertEqual(oauth.to_unicode(b'\xc2\xae'), _UGLYPH)
+
+    def test_to_utf8(self):
+        self.assertRaises(TypeError, oauth.to_utf8, 0)
+        self.assertRaises(TypeError, oauth.to_utf8, b'\x81')
+        self.assertEqual(oauth.to_utf8(_BSMILEY), _BSMILEY)
+        self.assertEqual(oauth.to_utf8(_UGLYPH),
+                             _UGLYPH.encode('utf8'))
+
+    def test_to_unicode_if_string(self):
+        self.assertTrue(oauth.to_unicode_if_string(self) is self)
+        self.assertEqual(oauth.to_unicode_if_string(_BSMILEY), _USMILEY)
+
+    def test_to_utf8_if_string(self):
+        self.assertTrue(oauth.to_utf8_if_string(self) is self)
+        self.assertEqual(oauth.to_utf8_if_string(_USMILEY), _BSMILEY)
+        self.assertEqual(oauth.to_utf8_if_string(_UGLYPH),
+                             _UGLYPH.encode('utf8'))
+
+    def test_to_unicode_optional_iterator(self):
+        self.assertEqual(oauth.to_unicode_optional_iterator(_BSMILEY),
+                             _USMILEY)
+        self.assertEqual(oauth.to_unicode_optional_iterator(_UGLYPH),
+                             _UGLYPH)
+        self.assertEqual(oauth.to_unicode_optional_iterator([_BSMILEY]),
+                             [_USMILEY])
+        self.assertEqual(oauth.to_unicode_optional_iterator([_UGLYPH]),
+                             [_UGLYPH])
+        self.assertEqual(oauth.to_unicode_optional_iterator((_UGLYPH,)),
+                             [_UGLYPH])
+        self.assertTrue(oauth.to_unicode_optional_iterator(self) is self)
+
+    def test_to_utf8_optional_iterator(self):
+        self.assertEqual(oauth.to_utf8_optional_iterator(_BSMILEY),
+                             _BSMILEY)
+        self.assertEqual(oauth.to_utf8_optional_iterator(_UGLYPH),
+                             _UGLYPH.encode('utf8'))
+        self.assertEqual(oauth.to_utf8_optional_iterator([_BSMILEY]),
+                             [_BSMILEY])
+        self.assertEqual(oauth.to_utf8_optional_iterator([_USMILEY]),
+                             [_BSMILEY])
+        self.assertEqual(oauth.to_utf8_optional_iterator([_UGLYPH]),
+                             [_UGLYPH.encode('utf8')])
+        self.assertEqual(oauth.to_utf8_optional_iterator((_UGLYPH,)),
+                             [_UGLYPH.encode('utf8')])
+        self.assertTrue(oauth.to_utf8_optional_iterator(self) is self)
+
+class TestRequest(unittest.TestCase, ReallyEqualMixin):
+    def test__init__(self):
+        method = "GET"
+        req = oauth.Request(method)
+        self.assertFalse('url' in req.__dict__)
+        self.assertFalse('normalized_url' in req.__dict__)
+        self.assertRaises(AttributeError, getattr, req, 'url')
+        self.assertRaises(AttributeError, getattr, req, 'normalized_url')
+
     def test_setter(self):
         url = "http://example.com"
         method = "GET"
-        req = oauth.Request(method)
-        self.assertTrue(req.url is None)
-        self.assertTrue(req.normalized_url is None)
+        req = oauth.Request(method, url)
+        self.assertEqual(req.url, url)
+        self.assertEqual(req.normalized_url, url)
+        req.url = url + '/?foo=bar'
+        self.assertEqual(req.url, url + '/?foo=bar')
+        self.assertEqual(req.normalized_url, url + '/')
+        req.url = None
+        self.assertEqual(req.url, None)
+        self.assertEqual(req.normalized_url, None)
 
     def test_deleter(self):
         url = "http://example.com"
         method = "GET"
         req = oauth.Request(method, url)
-
-        try:
-            del req.url
-            url = req.url
-            self.fail("AttributeError should have been raised on empty url.")
-        except AttributeError:
-            pass
-        except Exception, e:
-            self.fail(str(e))
+        del req.url
+        self.assertRaises(AttributeError, getattr, req, 'url')
 
     def test_url(self):
         url1 = "http://example.com:80/foo.php"
@@ -278,12 +357,12 @@ def test_url(self):
         method = "GET"
 
         req = oauth.Request(method, url1)
-        self.assertEquals(req.normalized_url, exp1)
-        self.assertEquals(req.url, url1)
+        self.assertEqual(req.normalized_url, exp1)
+        self.assertEqual(req.url, url1)
 
         req = oauth.Request(method, url2)
-        self.assertEquals(req.normalized_url, exp2)
-        self.assertEquals(req.url, url2)
+        self.assertEqual(req.normalized_url, exp2)
+        self.assertEqual(req.url, url2)
 
     def test_bad_url(self):
         request = oauth.Request()
@@ -300,31 +379,24 @@ def test_unset_consumer_and_token(self):
         request.sign_request(oauth.SignatureMethod_HMAC_SHA1(), consumer,
             token)
 
-        self.assertEquals(consumer.key, request['oauth_consumer_key'])
-        self.assertEquals(token.key, request['oauth_token'])
+        self.assertEqual(consumer.key, request['oauth_consumer_key'])
+        self.assertEqual(token.key, request['oauth_token'])
 
     def test_no_url_set(self):
         consumer = oauth.Consumer('my_consumer_key', 'my_consumer_secret')
         token = oauth.Token('my_key', 'my_secret')
         request = oauth.Request()
-
-        try:
-            try:
-                request.sign_request(oauth.SignatureMethod_HMAC_SHA1(), 
-                    consumer, token)
-            except TypeError:
-                self.fail("Signature method didn't check for a normalized URL.")
-        except ValueError:
-            pass
+        self.assertRaises(ValueError,
+                          request.sign_request,
+                          oauth.SignatureMethod_HMAC_SHA1(), consumer, token)
 
     def test_url_query(self):
-        url = "https://www.google.com/m8/feeds/contacts/default/full/?alt=json&max-contacts=10"
-        normalized_url = urlparse.urlunparse(urlparse.urlparse(url)[:3] + (None, None, None))
+        url = ("https://www.google.com/m8/feeds/contacts/default/full/?alt=json&max-contacts=10")
+        normalized_url = urlunparse(urlparse(url)[:3] + (None, None, None))
         method = "GET"
-        
         req = oauth.Request(method, url)
-        self.assertEquals(req.url, url)
-        self.assertEquals(req.normalized_url, normalized_url)
+        self.assertEqual(req.url, url)
+        self.assertEqual(req.normalized_url, normalized_url)
 
     def test_get_parameter(self):
         url = "http://example.com"
@@ -332,7 +404,7 @@ def test_get_parameter(self):
         params = {'oauth_consumer' : 'asdf'}
         req = oauth.Request(method, url, parameters=params)
 
-        self.assertEquals(req.get_parameter('oauth_consumer'), 'asdf')
+        self.assertEqual(req.get_parameter('oauth_consumer'), 'asdf')
         self.assertRaises(oauth.Error, req.get_parameter, 'blah')
 
     def test_get_nonoauth_parameters(self):
@@ -342,16 +414,83 @@ def test_get_nonoauth_parameters(self):
         }
 
         other_params = {
-            'foo': 'baz',
-            'bar': 'foo',
-            'multi': ['FOO','BAR']
+            u('foo'): u('baz'),
+            u('bar'): u('foo'),
+            u('multi'): [u('FOO'), u('BAR')],
+            u('uni_utf8'): u(b'\xae', 'latin1'),
+            u('uni_unicode'): _UGLYPH,
+            u('uni_unicode_2'):
+                u(b'\xc3\xa5\xc3\x85\xc3\xb8\xc3\x98', 'latin1'), # 'åÅøØ'
         }
 
         params = oauth_params
         params.update(other_params)
 
         req = oauth.Request("GET", "http://example.com", params)
-        self.assertEquals(other_params, req.get_nonoauth_parameters())
+        self.assertEqual(other_params, req.get_nonoauth_parameters())
+
+    def test_to_url_nonascii(self):
+        url = "http://sp.example.com/"
+
+        params = {
+            'nonasciithing': u'q\xbfu\xe9 ,aasp u?..a.s',
+            'oauth_version': "1.0",
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': "137131200",
+            'oauth_consumer_key': "0685bd9184jfhq22",
+            'oauth_signature_method': "HMAC-SHA1",
+            'oauth_token': "ad180jjd733klru7",
+            'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
+        }
+
+        req = oauth.Request("GET", url, params)
+        res = urlparse(req.to_url())
+
+        params['nonasciithing'] = params['nonasciithing'].encode('utf-8')
+        exp = urlparse("%s?%s" % (url, urlencode(params)))
+
+        self.assertEquals(exp.netloc, res.netloc)
+        self.assertEquals(exp.path, res.path)
+
+        a = parse_qs(exp.query)
+        b = parse_qs(res.query)
+        self.assertEquals(a, b)
+
+    def test_to_url_works_with_non_ascii_parameters(self):
+
+        oauth_params = {
+            'oauth_consumer': 'asdfasdfasdf'
+        }
+
+        other_params = {
+            u'foo': u'baz',
+            u'bar': u'foo',
+            u'uni_utf8': u'\xae',
+            u'uni_unicode': u'\u00ae',
+            u'uni_unicode_2': u'åÅøØ',
+        }
+
+        params = oauth_params
+        params.update(other_params)
+
+        req = oauth.Request("GET", "http://example.com", params)
+
+        # We need to split out the host and params and check individually since the order is not determinate.
+        url_parts = req.to_url().split("?")
+        host = url_parts[0]
+        params = dict(item.strip().split("=") for item in url_parts[1].split("&"))
+
+        expected_params = {
+            'uni_utf8': '%C2%AE',
+            'foo': 'baz',
+            'bar': 'foo',
+            'uni_unicode_2': '%C3%A5%C3%85%C3%B8%C3%98',
+            'uni_unicode': '%C2%AE',
+            'oauth_consumer': 'asdfasdfasdf'
+        }
+
+        self.assertEquals("http://example.com", host)
+        self.assertEquals(expected_params, params)
 
     def test_to_header(self):
         realm = "http://sp.example.com/"
@@ -367,7 +506,7 @@ def test_to_header(self):
         }
 
         req = oauth.Request("GET", realm, params)
-        header, value = req.to_header(realm).items()[0]
+        header, value = list(req.to_header(realm).items())[0]
 
         parts = value.split('OAuth ')
         vars = parts[1].split(', ')
@@ -376,15 +515,43 @@ def test_to_header(self):
         res = {}
         for v in vars:
             var, val = v.split('=')
-            res[var] = urllib.unquote(val.strip('"'))
+            res[var] = unquote(val.strip('"'))
 
-        self.assertEquals(realm, res['realm'])
+        self.assertEqual(realm, res['realm'])
         del res['realm']
 
         self.assertTrue(len(res), len(params))
 
         for key, val in res.items():
-            self.assertEquals(val, params.get(key))
+            self.assertEqual(val, params.get(key))
+
+    def test_to_postdata_nonascii(self):
+        realm = "http://sp.example.com/"
+
+        params = {
+            'nonasciithing': u('q\xbfu\xe9 ,aasp u?..a.s', 'latin1'),
+            'oauth_version': "1.0",
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': "137131200",
+            'oauth_consumer_key': "0685bd9184jfhq22",
+            'oauth_signature_method': "HMAC-SHA1",
+            'oauth_token': "ad180jjd733klru7",
+            'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
+        }
+
+        req = oauth.Request("GET", realm, params)
+
+        self.assertReallyEqual(
+            req.to_postdata(),
+            ('nonasciithing=q%C2%BFu%C3%A9%20%2Caasp%20u%3F..a.s'
+             '&oauth_consumer_key=0685bd9184jfhq22'
+             '&oauth_nonce=4572616e48616d6d65724c61686176'
+             '&oauth_signature=wOJIO9A2W5mFwDgiDvZbTSMK%252FPY%253D'
+             '&oauth_signature_method=HMAC-SHA1'
+             '&oauth_timestamp=137131200'
+             '&oauth_token=ad180jjd733klru7'
+             '&oauth_version=1.0'
+            ))
 
     def test_to_postdata(self):
         realm = "http://sp.example.com/"
@@ -406,7 +573,9 @@ def test_to_postdata(self):
         del params['multi']
         flat.extend(params.items())
         kf = lambda x: x[0]
-        self.assertEquals(sorted(flat, key=kf), sorted(parse_qsl(req.to_postdata()), key=kf))
+        self.assertEqual(
+                sorted(flat, key=kf),
+                sorted(parse_qsl(req.to_postdata()), key=kf))
 
     def test_to_url(self):
         url = "http://sp.example.com/"
@@ -422,18 +591,19 @@ def test_to_url(self):
         }
 
         req = oauth.Request("GET", url, params)
-        exp = urlparse.urlparse("%s?%s" % (url, urllib.urlencode(params)))
-        res = urlparse.urlparse(req.to_url())
-        self.assertEquals(exp.scheme, res.scheme)
-        self.assertEquals(exp.netloc, res.netloc)
-        self.assertEquals(exp.path, res.path)
+        exp = urlparse("%s?%s" % (url, urlencode(params)))
+        res = urlparse(req.to_url())
+        self.assertEqual(exp.scheme, res.scheme)
+        self.assertEqual(exp.netloc, res.netloc)
+        self.assertEqual(exp.path, res.path)
+
+        exp_parsed = parse_qs(exp.query)
+        res_parsed = parse_qs(res.query)
+        self.assertEqual(exp_parsed, res_parsed)
 
-        a = parse_qs(exp.query)
-        b = parse_qs(res.query)
-        self.assertEquals(a, b)
-    
     def test_to_url_with_query(self):
-        url = "https://www.google.com/m8/feeds/contacts/default/full/?alt=json&max-contacts=10"
+        url = ("https://www.google.com/m8/feeds/contacts/default/full/"
+               "?alt=json&max-contacts=10")
 
         params = {
             'oauth_version': "1.0",
@@ -446,23 +616,81 @@ def test_to_url_with_query(self):
         }
 
         req = oauth.Request("GET", url, params)
-        # Note: the url above already has query parameters, so append new ones with &
-        exp = urlparse.urlparse("%s&%s" % (url, urllib.urlencode(params)))
-        res = urlparse.urlparse(req.to_url())
-        self.assertEquals(exp.scheme, res.scheme)
-        self.assertEquals(exp.netloc, res.netloc)
-        self.assertEquals(exp.path, res.path)
+        # Note: the url above already has query parameters, so append new
+        # ones with &
+        exp = urlparse("%s&%s" % (url, urlencode(params)))
+        res = urlparse(req.to_url())
+        self.assertEqual(exp.scheme, res.scheme)
+        self.assertEqual(exp.netloc, res.netloc)
+        self.assertEqual(exp.path, res.path)
+
+        exp_q = parse_qs(exp.query)
+        res_q = parse_qs(res.query)
+        self.assertTrue('alt' in res_q)
+        self.assertTrue('max-contacts' in res_q)
+        self.assertEqual(res_q['alt'], ['json'])
+        self.assertEqual(res_q['max-contacts'], ['10'])
+        self.assertEqual(exp_q, res_q)
+
+    def test_signature_base_unicode_nonascii(self):
+        consumer = oauth.Consumer('consumer_token', 'consumer_secret')
 
-        a = parse_qs(exp.query)
-        b = parse_qs(res.query)
-        self.assertTrue('alt' in b)
-        self.assertTrue('max-contacts' in b)
-        self.assertEquals(b['alt'], ['json'])
-        self.assertEquals(b['max-contacts'], ['10'])
-        self.assertEquals(a, b)
+        url = u('http://api.simplegeo.com:80/1.0/places/address.json'
+                '?q=monkeys&category=animal'
+                '&address=41+Decatur+St,+San+Francisc') + _U2766 + u(',+CA')
+        req = oauth.Request("GET", url)
+        self.assertReallyEqual(
+            req.normalized_url,
+            u('http://api.simplegeo.com/1.0/places/address.json'))
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), consumer, None)
+        self.assertReallyEqual(
+            req['oauth_signature'], b'WhufgeZKyYpKsI70GZaiDaYwl6g=')
+
+    def test_signature_base_string_bytes_nonascii_nonutf8(self):
+        consumer = oauth.Consumer('consumer_token', 'consumer_secret')
+
+        url = (b'http://api.simplegeo.com:80/1.0/places/address.json'
+               b'?q=monkeys&category=animal'
+               b'&address=41+Decatur+St,+San+Francisc') + _B2766 + b',+CA'
+        req = oauth.Request("GET", url)
+        self.assertReallyEqual(
+            req.normalized_url,
+            u('http://api.simplegeo.com/1.0/places/address.json'))
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), consumer, None)
+        self.assertReallyEqual( #XXX
+            req['oauth_signature'], b'WhufgeZKyYpKsI70GZaiDaYwl6g=')
+
+    def test_signature_base_bytes_nonascii_nonutf8_urlencoded(self):
+        consumer = oauth.Consumer('consumer_token', 'consumer_secret')
+
+        url = (b'http://api.simplegeo.com:80/1.0/places/address.json'
+               b'?q=monkeys&category=animal'
+               b'&address=41+Decatur+St,+San+Francisc%E2%9D%A6,+CA')
+        req = oauth.Request("GET", url)
+        self.assertReallyEqual(
+            req.normalized_url,
+            u('http://api.simplegeo.com/1.0/places/address.json'))
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), consumer, None)
+        self.assertReallyEqual(
+            req['oauth_signature'], b'WhufgeZKyYpKsI70GZaiDaYwl6g=')
+
+    def test_signature_base_unicode_nonascii_nonutf8_url_encoded(self):
+        consumer = oauth.Consumer('consumer_token', 'consumer_secret')
+
+        url = u('http://api.simplegeo.com:80/1.0/places/address.json'
+                '?q=monkeys&category=animal'
+                '&address=41+Decatur+St,+San+Francisc%E2%9D%A6,+CA')
+        req = oauth.Request("GET", url)
+        self.assertReallyEqual(
+            req.normalized_url,
+            u('http://api.simplegeo.com/1.0/places/address.json'))
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), consumer, None)
+        self.assertReallyEqual(
+            req['oauth_signature'], b'WhufgeZKyYpKsI70GZaiDaYwl6g=')
 
     def test_signature_base_string_with_query(self):
-        url = "https://www.google.com/m8/feeds/contacts/default/full/?alt=json&max-contacts=10"
+        url = ("https://www.google.com/m8/feeds/contacts/default/full/"
+               "?alt=json&max-contacts=10")
         params = {
             'oauth_version': "1.0",
             'oauth_nonce': "4572616e48616d6d65724c61686176",
@@ -473,17 +701,111 @@ def test_signature_base_string_with_query(self):
             'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
         }
         req = oauth.Request("GET", url, params)
-        self.assertEquals(req.normalized_url, 'https://www.google.com/m8/feeds/contacts/default/full/')
-        self.assertEquals(req.url, 'https://www.google.com/m8/feeds/contacts/default/full/?alt=json&max-contacts=10')
+        self.assertEqual(
+            req.normalized_url,
+            'https://www.google.com/m8/feeds/contacts/default/full/')
+        self.assertEqual(req.url, url)
         normalized_params = parse_qsl(req.get_normalized_parameters())
         self.assertTrue(len(normalized_params), len(params) + 2)
         normalized_params = dict(normalized_params)
-        for key, value in params.iteritems():
+        for key, value in params.items():
             if key == 'oauth_signature':
                 continue
-            self.assertEquals(value, normalized_params[key])
-        self.assertEquals(normalized_params['alt'], 'json')
-        self.assertEquals(normalized_params['max-contacts'], '10')
+            self.assertEqual(value, normalized_params[key])
+        self.assertEqual(normalized_params['alt'], 'json')
+        self.assertEqual(normalized_params['max-contacts'], '10')
+
+    def test_get_normalized_parameters_empty(self):
+        url = "http://sp.example.com/?empty="
+
+        req = oauth.Request("GET", url)
+
+        res = req.get_normalized_parameters()
+
+        expected='empty='
+
+        self.assertEqual(expected, res)
+
+    def test_get_normalized_parameters_duplicate(self):
+        url = ("http://example.com/v2/search/videos"
+               "?oauth_nonce=79815175&oauth_timestamp=1295397962"
+               "&oauth_consumer_key=mykey&oauth_signature_method=HMAC-SHA1"
+               "&q=car&oauth_version=1.0&offset=10"
+               "&oauth_signature=spWLI%2FGQjid7sQVd5%2FarahRxzJg%3D")
+
+        req = oauth.Request("GET", url)
+
+        res = req.get_normalized_parameters()
+
+        expected = ('oauth_consumer_key=mykey&oauth_nonce=79815175'
+                    '&oauth_signature_method=HMAC-SHA1'
+                    '&oauth_timestamp=1295397962&oauth_version=1.0'
+                    '&offset=10&q=car')
+
+        self.assertEqual(expected, res)
+
+    def test_get_normalized_parameters_multiple(self):
+        url = "http://example.com/v2/search/videos?oauth_nonce=79815175&oauth_timestamp=1295397962&oauth_consumer_key=mykey&oauth_signature_method=HMAC-SHA1&oauth_version=1.0&offset=10&oauth_signature=spWLI%2FGQjid7sQVd5%2FarahRxzJg%3D&tag=one&tag=two"
+
+        req = oauth.Request("GET", url)
+
+        res = req.get_normalized_parameters()
+
+        expected='oauth_consumer_key=mykey&oauth_nonce=79815175&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1295397962&oauth_version=1.0&offset=10&tag=one&tag=two'
+
+        self.assertEqual(expected, res)
+
+
+    def test_get_normalized_parameters_from_url(self):
+        # example copied from
+        # https://github.com/ciaranj/node-oauth/blob/master/tests/oauth.js
+        # which in turns says that it was copied from
+        # http://oauth.net/core/1.0/#sig_base_example .
+        url = ("http://photos.example.net/photos?file=vacation.jpg"
+               "&oauth_consumer_key=dpf43f3p2l4k3l03"
+               "&oauth_nonce=kllo9940pd9333jh&oauth_signature_method=HMAC-SHA1"
+               "&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk"
+               "&oauth_version=1.0&size=original")
+
+        req = oauth.Request("GET", url)
+
+        res = req.get_normalized_parameters()
+
+        expected = ('file=vacation.jpg&oauth_consumer_key=dpf43f3p2l4k3l03'
+                    '&oauth_nonce=kllo9940pd9333jh'
+                    '&oauth_signature_method=HMAC-SHA1'
+                    '&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk'
+                    '&oauth_version=1.0&size=original')
+
+        self.assertEqual(expected, res)
+
+    def test_signing_base(self):
+        # example copied from
+        # https://github.com/ciaranj/node-oauth/blob/master/tests/oauth.js
+        # which in turns says that it was copied from
+        # http://oauth.net/core/1.0/#sig_base_example .
+        url = ("http://photos.example.net/photos?file=vacation.jpg"
+               "&oauth_consumer_key=dpf43f3p2l4k3l03"
+               "&oauth_nonce=kllo9940pd9333jh&oauth_signature_method=HMAC-SHA1"
+               "&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk"
+               "&oauth_version=1.0&size=original")
+
+        req = oauth.Request("GET", url)
+
+        sm = oauth.SignatureMethod_HMAC_SHA1()
+
+        consumer = oauth.Consumer('dpf43f3p2l4k3l03', 'foo')
+        key, raw = sm.signing_base(req, consumer, None)
+
+        expected = b('GET&http%3A%2F%2Fphotos.example.net%2Fphotos'
+                     '&file%3Dvacation.jpg'
+                     '%26oauth_consumer_key%3Ddpf43f3p2l4k3l03'
+                     '%26oauth_nonce%3Dkllo9940pd9333jh'
+                     '%26oauth_signature_method%3DHMAC-SHA1'
+                     '%26oauth_timestamp%3D1191242096'
+                     '%26oauth_token%3Dnnch734d00sl2jdk'
+                     '%26oauth_version%3D1.0%26size%3Doriginal')
+        self.assertEqual(expected, raw)
 
     def test_get_normalized_parameters(self):
         url = "http://sp.example.com/"
@@ -495,16 +817,27 @@ def test_get_normalized_parameters(self):
             'oauth_consumer_key': "0685bd9184jfhq22",
             'oauth_signature_method': "HMAC-SHA1",
             'oauth_token': "ad180jjd733klru7",
-            'multi': ['FOO','BAR'],
+            'multi': ['FOO','BAR', _UGLYPH, b'\xc2\xae'],
+            'multi_same': ['FOO','FOO'],
+            'uni_utf8_bytes': b'\xc2\xae',
+            'uni_unicode_object': _UGLYPH
         }
 
         req = oauth.Request("GET", url, params)
 
         res = req.get_normalized_parameters()
-        
-        srtd = [(k, v if type(v) != ListType else sorted(v)) for k,v in sorted(params.items())]
 
-        self.assertEquals(urllib.urlencode(srtd, True), res)
+        expected = ('multi=BAR&multi=FOO&multi=%C2%AE&multi=%C2%AE'
+                    '&multi_same=FOO&multi_same=FOO'
+                    '&oauth_consumer_key=0685bd9184jfhq22'
+                    '&oauth_nonce=4572616e48616d6d65724c61686176'
+                    '&oauth_signature_method=HMAC-SHA1'
+                    '&oauth_timestamp=137131200'
+                    '&oauth_token=ad180jjd733klru7'
+                    '&oauth_version=1.0'
+                    '&uni_unicode_object=%C2%AE&uni_utf8_bytes=%C2%AE')
+
+        self.assertEqual(expected, res)
 
     def test_get_normalized_parameters_ignores_auth_signature(self):
         url = "http://sp.example.com/"
@@ -523,11 +856,17 @@ def test_get_normalized_parameters_ignores_auth_signature(self):
 
         res = req.get_normalized_parameters()
 
-        self.assertNotEquals(urllib.urlencode(sorted(params.items())), res)
+        self.assertNotEqual(urlencode(sorted(params.items())), res)
 
         foo = params.copy()
         del foo["oauth_signature"]
-        self.assertEqual(urllib.urlencode(sorted(foo.items())), res)
+        self.assertEqual(urlencode(sorted(foo.items())), res)
+        
+    def test_signature_base_string_with_matrix_params(self):
+        url = "http://social.yahooapis.com/v1/user/6677/connections;start=0;count=20"
+        req = oauth.Request("GET", url, None)
+        self.assertEqual(req.normalized_url, 'http://social.yahooapis.com/v1/user/6677/connections;start=0;count=20')
+        self.assertEqual(req.url, 'http://social.yahooapis.com/v1/user/6677/connections;start=0;count=20')
 
     def test_set_signature_method(self):
         consumer = oauth.Consumer('key', 'secret')
@@ -544,20 +883,136 @@ class Blah:
 
         m = oauth.SignatureMethod_HMAC_SHA1()
         client.set_signature_method(m)
-        self.assertEquals(m, client.method)
+        self.assertEqual(m, client.method)
 
     def test_get_normalized_string_escapes_spaces_properly(self):
         url = "http://sp.example.com/"
         params = {
             "some_random_data": random.randint(100, 1000),
-            "data": "This data with a random number (%d) has spaces!" % random.randint(1000, 2000),
+            "data": "This data with a random number (%d) has spaces!"
+                                    % random.randint(1000, 2000),
         }
 
         req = oauth.Request("GET", url, params)
         res = req.get_normalized_parameters()
-        expected = urllib.urlencode(sorted(params.items())).replace('+', '%20')
+        expected = urlencode(sorted(params.items())).replace('+', '%20')
         self.assertEqual(expected, res)
 
+    @mock.patch('oauth2.Request.make_timestamp')
+    @mock.patch('oauth2.Request.make_nonce')
+    def test_request_nonutf8_bytes(self, mock_make_nonce, mock_make_timestamp):
+        mock_make_nonce.return_value = 5
+        mock_make_timestamp.return_value = 6
+
+        tok = oauth.Token(key="tok-test-key", secret="tok-test-secret")
+        con = oauth.Consumer(key="con-test-key", secret="con-test-secret")
+        params = {
+            'oauth_version': "1.0",
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': "137131200",
+            'oauth_token': tok.key,
+            'oauth_consumer_key': con.key
+        }
+
+        if not PY3:
+            # If someone passes a sequence of bytes which is not ascii for
+            # url, we'll raise an exception as early as possible.
+            url = "http://sp.example.com/\x92" # It's actually cp1252-encoding...
+            self.assertRaises(TypeError, oauth.Request, method="GET", url=url, parameters=params)
+
+        # And if they pass an unicode, then we'll use it.
+        url = u('http://sp.example.com/') + _U2019
+        req = oauth.Request(method="GET", url=url, parameters=params)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None)
+        self.assertReallyEqual(req['oauth_signature'], b'cMzvCkhvLL57+sTIxLITTHfkqZk=')
+
+        # And if it is a utf-8-encoded-then-percent-encoded non-ascii
+        # thing, we'll decode it and use it.
+        url = "http://sp.example.com/%E2%80%99"
+        req = oauth.Request(method="GET", url=url, parameters=params)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None)
+        self.assertReallyEqual(req['oauth_signature'], b'yMLKOyNKC/DkyhUOb8DLSvceEWE=')
+
+        # Same thing with the params.
+        url = "http://sp.example.com/"
+
+        # If someone passes a sequence of bytes which is not ascii in
+        # params, we'll raise an exception as early as possible.
+        params['non_oauth_thing'] = b'\xae', # It's actually cp1252-encoding...
+        self.assertRaises(TypeError, oauth.Request, method="GET", url=url, parameters=params)
+
+        # And if they pass a unicode, then we'll use it.
+        params['non_oauth_thing'] = _U2019
+        req = oauth.Request(method="GET", url=url, parameters=params)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None)
+        self.assertReallyEqual(req['oauth_signature'], b'0GU50m0v60CVDB5JnoBXnvvvKx4=')
+
+        # And if it is a utf-8-encoded non-ascii thing, we'll decode
+        # it and use it.
+        params['non_oauth_thing'] = b'\xc2\xae'
+        req = oauth.Request(method="GET", url=url, parameters=params)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None)
+        self.assertReallyEqual(req['oauth_signature'], b'pqOCu4qvRTiGiXB8Z61Jsey0pMM=')
+
+
+        # Also if there are non-utf8 bytes in the query args.
+        url = b"http://sp.example.com/?q=\x92" # cp1252
+        self.assertRaises(TypeError, oauth.Request, method="GET", url=url, parameters=params)
+
+    def test_request_hash_of_body(self):
+        tok = oauth.Token(key="token", secret="tok-test-secret")
+        con = oauth.Consumer(key="consumer", secret="con-test-secret")
+
+        # Example 1a from Appendix A.1 of
+        # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
+        # Except that we get a differetn result than they do.
+
+        params = {
+            'oauth_version': "1.0",
+            'oauth_token': tok.key,
+            'oauth_nonce': 10288510250934,
+            'oauth_timestamp': 1236874155,
+            'oauth_consumer_key': con.key
+        }
+
+        url = u('http://www.example.com/resource')
+        req = oauth.Request(method="PUT", url=url, parameters=params, body=b"Hello World!", is_form_encoded=False)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None)
+        self.assertReallyEqual(req['oauth_body_hash'], b'Lve95gjOVATpfV8EL5X4nxwjKHE=')
+        self.assertReallyEqual(req['oauth_signature'], b't+MX8l/0S8hdbVQL99nD0X1fPnM=')
+        # oauth-bodyhash.html A.1 has
+        # '08bUFF%2Fjmp59mWB7cSgCYBUpJ0U%3D', but I don't see how that
+        # is possible.
+
+        # Example 1b
+        params = {
+            'oauth_version': "1.0",
+            'oauth_token': tok.key,
+            'oauth_nonce': 10369470270925,
+            'oauth_timestamp': 1236874236,
+            'oauth_consumer_key': con.key
+        }
+
+        req = oauth.Request(method="PUT", url=url, parameters=params, body=b"Hello World!", is_form_encoded=False)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None)
+        self.assertReallyEqual(req['oauth_body_hash'], b'Lve95gjOVATpfV8EL5X4nxwjKHE=')
+        self.assertReallyEqual(req['oauth_signature'], b'CTFmrqJIGT7NsWJ42OrujahTtTc=')
+
+        # Appendix A.2
+        params = {
+            'oauth_version': "1.0",
+            'oauth_token': tok.key,
+            'oauth_nonce': 8628868109991,
+            'oauth_timestamp': 1238395022,
+            'oauth_consumer_key': con.key
+        }
+
+        req = oauth.Request(method="GET", url=url, parameters=params, is_form_encoded=False)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None)
+        self.assertReallyEqual(req['oauth_body_hash'], b'2jmj7l5rSw0yVb/vlWAYkK/YBwk=')
+        self.assertReallyEqual(req['oauth_signature'], b'Zhl++aWSP0O3/hYQ0CuBc7jv38I=')
+
+
     def test_sign_request(self):
         url = "http://sp.example.com/"
 
@@ -575,14 +1030,93 @@ def test_sign_request(self):
         req = oauth.Request(method="GET", url=url, parameters=params)
 
         methods = {
-            'TQ6vGQ5A6IZn8dmeGB4+/Jl3EMI=': oauth.SignatureMethod_HMAC_SHA1(),
-            'con-test-secret&tok-test-secret': oauth.SignatureMethod_PLAINTEXT()
-        }
+            b'DX01TdHws7OninCLK9VztNTH1M4=': oauth.SignatureMethod_HMAC_SHA1(),
+            b'con-test-secret&tok-test-secret': oauth.SignatureMethod_PLAINTEXT()
+            }
 
         for exp, method in methods.items():
             req.sign_request(method, con, tok)
-            self.assertEquals(req['oauth_signature_method'], method.name)
-            self.assertEquals(req['oauth_signature'], exp)
+            self.assertEqual(req['oauth_signature_method'], method.name)
+            self.assertEqual(req['oauth_signature'], exp)
+
+        # Also if there are non-ascii chars in the URL.
+        url = b"http://sp.example.com/\xe2\x80\x99" # utf-8 bytes
+        req = oauth.Request(method="GET", url=url, parameters=params)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, tok)
+        self.assertEqual(req['oauth_signature'], b'loFvp5xC7YbOgd9exIO6TxB7H4s=')
+
+        url = u('http://sp.example.com/') + _U2019 # Python unicode object
+        req = oauth.Request(method="GET", url=url, parameters=params)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, tok)
+        self.assertEqual(req['oauth_signature'], b'loFvp5xC7YbOgd9exIO6TxB7H4s=')
+
+        # Also if there are non-ascii chars in the query args.
+        url = b"http://sp.example.com/?q=\xe2\x80\x99" # utf-8 bytes
+        req = oauth.Request(method="GET", url=url, parameters=params)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, tok)
+        self.assertEqual(req['oauth_signature'], b'IBw5mfvoCsDjgpcsVKbyvsDqQaU=')
+
+        url = u('http://sp.example.com/?q=') + _U2019 # Python unicode object
+        req = oauth.Request(method="GET", url=url, parameters=params)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, tok)
+        self.assertEqual(req['oauth_signature'], b'IBw5mfvoCsDjgpcsVKbyvsDqQaU=')
+
+
+    def test_from_request_works_with_wsgi(self):
+        """Make sure WSGI header HTTP_AUTHORIZATION is detected correctly."""
+        url = "http://sp.example.com/"
+
+        params = {
+            'oauth_version': "1.0",
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': "137131200",
+            'oauth_consumer_key': "0685bd9184jfhq22",
+            'oauth_signature_method': "HMAC-SHA1",
+            'oauth_token': "ad180jjd733klru7",
+            'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
+        }
+
+        req = oauth.Request("GET", url, params)
+        headers = req.to_header()
+
+        # Munge the headers
+        headers['HTTP_AUTHORIZATION'] = headers['Authorization']
+        del headers['Authorization'] 
+
+        # Test from the headers
+        req = oauth.Request.from_request("GET", url, headers)
+        self.assertEqual(req.method, "GET")
+        self.assertEqual(req.url, url)
+        self.assertEqual(params, req.copy())
+
+
+    def test_from_request_is_case_insensitive_checking_for_auth(self):
+        """Checks for the Authorization header should be case insensitive."""
+        url = "http://sp.example.com/"
+
+        params = {
+            'oauth_version': "1.0",
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': "137131200",
+            'oauth_consumer_key': "0685bd9184jfhq22",
+            'oauth_signature_method': "HMAC-SHA1",
+            'oauth_token': "ad180jjd733klru7",
+            'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
+        }
+
+        req = oauth.Request("GET", url, params)
+        headers = req.to_header()
+
+        # Munge the headers
+        headers['authorization'] = headers['Authorization']
+        del headers['Authorization'] 
+
+        # Test from the headers
+        req = oauth.Request.from_request("GET", url, headers)
+        self.assertEqual(req.method, "GET")
+        self.assertEqual(req.url, url)
+        self.assertEqual(params, req.copy())
+
 
     def test_from_request(self):
         url = "http://sp.example.com/"
@@ -602,10 +1136,10 @@ def test_from_request(self):
 
         # Test from the headers
         req = oauth.Request.from_request("GET", url, headers)
-        self.assertEquals(req.method, "GET")
-        self.assertEquals(req.url, url)
+        self.assertEqual(req.method, "GET")
+        self.assertEqual(req.url, url)
 
-        self.assertEquals(params, req.copy())
+        self.assertEqual(params, req.copy())
 
         # Test with bad OAuth headers
         bad_headers = {
@@ -616,18 +1150,18 @@ def test_from_request(self):
             url, bad_headers)
 
         # Test getting from query string
-        qs = urllib.urlencode(params)
+        qs = urlencode(params)
         req = oauth.Request.from_request("GET", url, query_string=qs)
 
         exp = parse_qs(qs, keep_blank_values=False)
-        for k, v in exp.iteritems():
-            exp[k] = urllib.unquote(v[0])
+        for k, v in exp.items():
+            exp[k] = unquote(v[0])
 
-        self.assertEquals(exp, req.copy())
+        self.assertEqual(exp, req.copy())
 
         # Test that a boned from_request() call returns None
         req = oauth.Request.from_request("GET", url)
-        self.assertEquals(None, req)
+        self.assertEqual(None, req)
 
     def test_from_token_and_callback(self):
         url = "http://sp.example.com/"
@@ -645,11 +1179,11 @@ def test_from_token_and_callback(self):
         tok = oauth.Token(key="tok-test-key", secret="tok-test-secret")
         req = oauth.Request.from_token_and_callback(tok)
         self.assertFalse('oauth_callback' in req)
-        self.assertEquals(req['oauth_token'], tok.key)
+        self.assertEqual(req['oauth_token'], tok.key)
 
         req = oauth.Request.from_token_and_callback(tok, callback=url)
         self.assertTrue('oauth_callback' in req)
-        self.assertEquals(req['oauth_callback'], url)
+        self.assertEqual(req['oauth_callback'], url)
 
     def test_from_consumer_and_token(self):
         url = "http://sp.example.com/"
@@ -660,9 +1194,9 @@ def test_from_consumer_and_token(self):
         req = oauth.Request.from_consumer_and_token(con, token=tok,
             http_method="GET", http_url=url)
 
-        self.assertEquals(req['oauth_token'], tok.key)
-        self.assertEquals(req['oauth_consumer_key'], con.key)
-        self.assertEquals(tok.verifier, req['oauth_verifier'])
+        self.assertEqual(req['oauth_token'], tok.key)
+        self.assertEqual(req['oauth_consumer_key'], con.key)
+        self.assertEqual(tok.verifier, req['oauth_verifier'])
 
 class SignatureMethod_Bad(oauth.SignatureMethod):
     name = "BAD"
@@ -676,7 +1210,7 @@ def sign(self, request, consumer, token):
 
 class TestServer(unittest.TestCase):
     def setUp(self):
-        url = "http://sp.example.com/"
+        self.url = "http://sp.example.com/"
 
         params = {
             'oauth_version': "1.0",
@@ -693,7 +1227,7 @@ def setUp(self):
 
         params['oauth_token'] = self.token.key
         params['oauth_consumer_key'] = self.consumer.key
-        self.request = oauth.Request(method="GET", url=url, parameters=params)
+        self.request = oauth.Request(method="GET", url=self.url, parameters=params)
 
         signature_method = oauth.SignatureMethod_HMAC_SHA1()
         self.request.sign_request(signature_method, self.consumer, self.token)
@@ -705,7 +1239,7 @@ def test_init(self):
             oauth.SignatureMethod_HMAC_SHA1))
 
         server = oauth.Server()
-        self.assertEquals(server.signature_methods, {})
+        self.assertEqual(server.signature_methods, {})
 
     def test_add_signature_method(self):
         server = oauth.Server()
@@ -731,15 +1265,53 @@ def test_verify_request(self):
         self.assertTrue('bar' in parameters)
         self.assertTrue('foo' in parameters)
         self.assertTrue('multi' in parameters)
-        self.assertEquals(parameters['bar'], 'blerg')
-        self.assertEquals(parameters['foo'], 59)
-        self.assertEquals(parameters['multi'], ['FOO','BAR'])
+        self.assertEqual(parameters['bar'], 'blerg')
+        self.assertEqual(parameters['foo'], 59)
+        self.assertEqual(parameters['multi'], ['FOO','BAR'])
+
+    def test_verify_request_query_string(self):
+        server = oauth.Server()
+        server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1())
+
+        signature_method = oauth.SignatureMethod_HMAC_SHA1()
+        request2 = oauth.Request.from_request("GET", self.url, query_string=urlencode(dict(self.request)))
+        request2.sign_request(signature_method, self.consumer, self.token)
+        request3 = oauth.Request.from_request("GET", self.url, query_string=urlencode(dict(request2)))        
+
+        parameters = server.verify_request(request3, self.consumer,
+            self.token)
+
+    def test_verify_request_missing_signature(self):
+        from oauth2 import MissingSignature
+        server = oauth.Server()
+        server.add_signature_method(oauth.SignatureMethod_PLAINTEXT())
+        del self.request['oauth_signature_method']
+        del self.request['oauth_signature']
+
+        self.assertRaises(MissingSignature,
+               server.verify_request, self.request, self.consumer, self.token)
+
+    def test_verify_request_invalid_signature(self):
+        server = oauth.Server()
+        server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1())
+        self.request['oauth_signature'] = 'BOGUS'
+
+        self.assertRaises(oauth.Error,
+               server.verify_request, self.request, self.consumer, self.token)
+
+    def test_verify_request_invalid_timestamp(self):
+        server = oauth.Server()
+        server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1())
+        self.request['oauth_timestamp'] -= 86400
+
+        self.assertRaises(oauth.Error,
+               server.verify_request, self.request, self.consumer, self.token)
 
     def test_build_authenticate_header(self):
         server = oauth.Server()
         headers = server.build_authenticate_header('example.com')
         self.assertTrue('WWW-Authenticate' in headers)
-        self.assertEquals('OAuth realm="example.com"', 
+        self.assertEqual('OAuth realm="example.com"',
             headers['WWW-Authenticate'])
 
     def test_no_version(self):
@@ -796,8 +1368,7 @@ def test_invalid_version(self):
         server = oauth.Server()
         server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1())
 
-        self.assertRaises(oauth.Error, server.verify_request, request,
-            consumer, token)
+        self.assertRaises(oauth.Error, server.verify_request, request, consumer, token)
 
     def test_invalid_signature_method(self):
         url = "http://sp.example.com/"
@@ -885,7 +1456,6 @@ class TestClient(unittest.TestCase):
     host = 'http://oauth-sandbox.sevengoslings.net'
 
     def setUp(self):
-        self.mox = mox.Mox()
         self.consumer = oauth.Consumer(key=self.consumer_key,
             secret=self.consumer_secret)
 
@@ -896,9 +1466,6 @@ def setUp(self):
             'blah': 599999
         }
 
-    def tearDown(self):
-        self.mox.UnsetStubs()
-
     def _uri(self, type):
         uri = self.oauth_uris.get(type)
         if uri is None:
@@ -910,7 +1477,7 @@ def create_simple_multipart_data(self, data):
         boundary = '---Boundary-%d' % random.randint(1,1000)
         crlf = '\r\n'
         items = []
-        for key, value in data.iteritems():
+        for key, value in data.items():
             items += [
                 '--'+boundary,
                 'Content-Disposition: form-data; name="%s"'%str(key),
@@ -919,7 +1486,7 @@ def create_simple_multipart_data(self, data):
             ]
         items += ['', '--'+boundary+'--', '']
         content_type = 'multipart/form-data; boundary=%s' % boundary
-        return content_type, crlf.join(items)
+        return content_type, crlf.join(items).encode('ascii')
 
     def test_init(self):
         class Blah():
@@ -938,43 +1505,55 @@ class Blah():
         except ValueError:
             pass
 
+    def test_init_passes_kwargs_to_httplib2(self):
+        class Blah():
+            pass
+ 
+        consumer = oauth.Consumer('token', 'secret')
+ 
+        # httplib2 options
+        client = oauth.Client(consumer, None, cache='.cache', timeout=3, disable_ssl_certificate_validation=True)
+        self.assertNotEqual(client.cache, None)
+        self.assertEqual(client.timeout, 3)
+
+
     def test_access_token_get(self):
         """Test getting an access token via GET."""
         client = oauth.Client(self.consumer, None)
         resp, content = client.request(self._uri('request_token'), "GET")
 
-        self.assertEquals(int(resp['status']), 200)
+        self.assertEqual(int(resp['status']), 200)
 
     def test_access_token_post(self):
         """Test getting an access token via POST."""
         client = oauth.Client(self.consumer, None)
         resp, content = client.request(self._uri('request_token'), "POST")
 
-        self.assertEquals(int(resp['status']), 200)
+        self.assertEqual(int(resp['status']), 200)
 
         res = dict(parse_qsl(content))
-        self.assertTrue('oauth_token' in res)
-        self.assertTrue('oauth_token_secret' in res)
+        self.assertTrue(b'oauth_token' in res)
+        self.assertTrue(b'oauth_token_secret' in res)
 
     def _two_legged(self, method):
         client = oauth.Client(self.consumer, None)
 
-        return client.request(self._uri('two_legged'), method,
-            body=urllib.urlencode(self.body))
+        body = urlencode(self.body).encode('ascii')
+        return client.request(self._uri('two_legged'), method, body=body)
 
     def test_two_legged_post(self):
         """A test of a two-legged OAuth POST request."""
         resp, content = self._two_legged("POST")
 
-        self.assertEquals(int(resp['status']), 200)
+        self.assertEqual(int(resp['status']), 200)
 
     def test_two_legged_get(self):
         """A test of a two-legged OAuth GET request."""
         resp, content = self._two_legged("GET")
-        self.assertEquals(int(resp['status']), 200)
+        self.assertEqual(int(resp['status']), 200)
 
-    def test_multipart_post_does_not_alter_body(self):
-        self.mox.StubOutWithMock(httplib2.Http, 'request')
+    @mock.patch('httplib2.Http.request')
+    def test_multipart_post_does_not_alter_body(self, mockHttpRequest):
         random_result = random.randint(1,100)
 
         data = {
@@ -985,50 +1564,82 @@ def test_multipart_post_does_not_alter_body(self):
         client = oauth.Client(self.consumer, None)
         uri = self._uri('two_legged')
 
-        expected_kwargs = {
-            'method':'POST',
-            'body':body,
-            'redirections':httplib2.DEFAULT_MAX_REDIRECTS,
-            'connection_type':None,
-            'headers':mox.IsA(dict),
-        }
-        httplib2.Http.request(client, uri, **expected_kwargs).AndReturn(random_result)
+        def mockrequest(cl, ur, **kw):
+            self.assertTrue(cl is client)
+            self.assertTrue(ur is uri)
+            self.assertEqual(frozenset(kw.keys()), frozenset(['method', 'body', 'redirections', 'connection_type', 'headers']))
+            self.assertEqual(kw['body'], body)
+            self.assertEqual(kw['connection_type'], None)
+            self.assertEqual(kw['method'], 'POST')
+            self.assertEqual(kw['redirections'],
+                                 httplib2.DEFAULT_MAX_REDIRECTS)
+            self.assertTrue(isinstance(kw['headers'], dict))
 
-        self.mox.ReplayAll()
-        result = client.request(uri, 'POST', headers={'Content-Type':content_type}, body=body)
+            return random_result
+
+        mockHttpRequest.side_effect = mockrequest
+
+        result = client.request(uri, 'POST',
+                                headers={'Content-Type':content_type},
+                                body=body)
         self.assertEqual(result, random_result)
-        self.mox.VerifyAll()
 
-    def test_url_with_query_string(self):
-        self.mox.StubOutWithMock(httplib2.Http, 'request')
+    @mock.patch('httplib2.Http.request')
+    def test_url_with_query_string(self, mockHttpRequest):
         uri = 'http://example.com/foo/bar/?show=thundercats&character=snarf'
         client = oauth.Client(self.consumer, None)
-        expected_kwargs = {
-            'method': 'GET',
-            'body': None,
-            'redirections': httplib2.DEFAULT_MAX_REDIRECTS,
-            'connection_type': None,
-            'headers': mox.IsA(dict),
-        }
-        def oauth_verifier(url):
+        random_result = random.randint(1,100)
+
+        def mockrequest(cl, ur, **kw):
+            self.assertTrue(cl is client)
+            self.assertEqual(frozenset(kw.keys()),
+                                 frozenset(['method', 'body', 'redirections',
+                                            'connection_type', 'headers']))
+            self.assertEqual(kw['body'], b'')
+            self.assertEqual(kw['connection_type'], None)
+            self.assertEqual(kw['method'], 'GET')
+            self.assertEqual(kw['redirections'],
+                                 httplib2.DEFAULT_MAX_REDIRECTS)
+            self.assertTrue(isinstance(kw['headers'], dict))
+
             req = oauth.Request.from_consumer_and_token(self.consumer, None,
                     http_method='GET', http_url=uri, parameters={})
-            req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), self.consumer, None)
-            expected = parse_qsl(urlparse.urlparse(req.to_url()).query)
-            actual = parse_qsl(urlparse.urlparse(url).query)
-            if len(expected) != len(actual):
-                return False
+            req.sign_request(oauth.SignatureMethod_HMAC_SHA1(),
+                             self.consumer, None)
+            expected = parse_qsl(
+                            urlparse(req.to_url()).query)
+            actual = parse_qsl(urlparse(ur).query)
+            self.assertEqual(len(expected), len(actual))
             actual = dict(actual)
             for key, value in expected:
-                if key not in ('oauth_signature', 'oauth_nonce', 'oauth_timestamp'):
-                    if actual[key] != value:
-                        return False
-            return True
-        httplib2.Http.request(client, mox.Func(oauth_verifier), **expected_kwargs)
-        self.mox.ReplayAll()
+                if key not in ('oauth_signature',
+                               'oauth_nonce', 'oauth_timestamp'):
+                    self.assertEqual(actual[key], value)
+
+            return random_result
+
+        mockHttpRequest.side_effect = mockrequest
+
         client.request(uri, 'GET')
-        self.mox.VerifyAll()
+
+    @mock.patch('httplib2.Http.request')
+    @mock.patch('oauth2.Request.from_consumer_and_token')
+    def test_multiple_values_for_a_key(self, mockReqConstructor, mockHttpRequest):
+        client = oauth.Client(self.consumer, None)
+
+        request = oauth.Request("GET", "http://example.com/fetch.php", parameters={'multi': ['1', '2']})
+        mockReqConstructor.return_value = request
+
+        client.request('http://whatever', 'POST', body='multi=1&multi=2')
+
+        self.assertEqual(mockReqConstructor.call_count, 1)
+        self.assertEqual(mockReqConstructor.call_args[1]['parameters'], {'multi': ['1', '2']})
+
+        self.assertTrue('multi=1' in mockHttpRequest.call_args[1]['body'])
+        self.assertTrue('multi=2' in mockHttpRequest.call_args[1]['body'])
 
 if __name__ == "__main__":
+    import os
+    import sys
+    sys.path[0:0] = [os.path.join(os.path.dirname(__file__), ".."),]
     unittest.main()
-
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 00000000..8f949280
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,37 @@
+[tox]
+envlist = 
+    py26,py27,py32,py33,cover
+
+[testenv]
+commands = 
+    python setup.py test -q
+deps =
+    httplib2
+    coverage
+    mock
+
+[testenv:cover]
+basepython =
+    python2.7
+commands = 
+    nosetests --with-xunit --with-xcoverage
+deps =
+    httplib2
+    coverage
+    mock
+    nose
+    nosexcover
+
+# we separate coverage into its own testenv because a) "last run wins" wrt
+# cobertura jenkins reporting and b) pypy and jython can't handle any
+# combination of versions of coverage and nosexcover that i can find.
+
+[testenv:docs]
+basepython =
+    python2.6
+commands = 
+    sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html
+    sphinx-build -b doctest -d docs/_build/doctrees docs docs/_build/doctest
+deps =
+    Sphinx
+    repoze.sphinx.autointerface
