Flows no longer need to be saved between uses.

Also introduces util.positional declarations.

Reviewed in http://codereview.appspot.com/6441056/.

Fixes issue #136.
diff --git a/oauth2client/appengine.py b/oauth2client/appengine.py
index 12ba04d..6f63831 100644
--- a/oauth2client/appengine.py
+++ b/oauth2client/appengine.py
@@ -27,21 +27,20 @@
 
 import clientsecrets
 
-from anyjson import simplejson
-from client import AccessTokenRefreshError
-from client import AssertionCredentials
-from client import Credentials
-from client import Flow
-from client import OAuth2WebServerFlow
-from client import Storage
-from google.appengine.api import memcache
-from google.appengine.api import users
 from google.appengine.api import app_identity
+from google.appengine.api import users
 from google.appengine.ext import db
 from google.appengine.ext import webapp
 from google.appengine.ext.webapp.util import login_required
 from google.appengine.ext.webapp.util import run_wsgi_app
-
+from oauth2client import util
+from oauth2client.anyjson import simplejson
+from oauth2client.client import AccessTokenRefreshError
+from oauth2client.client import AssertionCredentials
+from oauth2client.client import Credentials
+from oauth2client.client import Flow
+from oauth2client.client import OAuth2WebServerFlow
+from oauth2client.client import Storage
 
 logger = logging.getLogger(__name__)
 
@@ -66,6 +65,7 @@
   generate and refresh its own access tokens.
   """
 
+  @util.positional(2)
   def __init__(self, scope, **kwargs):
     """Constructor for AppAssertionCredentials
 
@@ -77,9 +77,8 @@
     self.scope = scope
 
     super(AppAssertionCredentials, self).__init__(
-        None,
-        None,
-        None)
+        'ignored' # assertion_type is ignore in this subclass.
+        )
 
   @classmethod
   def from_json(cls, json):
@@ -195,6 +194,7 @@
   are stored by key_name.
   """
 
+  @util.positional(4)
   def __init__(self, model, key_name, property_name, cache=None):
     """Constructor for Storage.
 
@@ -286,11 +286,14 @@
 
   """
 
+  @util.positional(4)
   def __init__(self, client_id, client_secret, scope,
                auth_uri='https://accounts.google.com/o/oauth2/auth',
                token_uri='https://accounts.google.com/o/oauth2/token',
                user_agent=None,
-               message=None, **kwargs):
+               message=None,
+               callback_path='/oauth2callback',
+               **kwargs):
 
     """Constructor for OAuth2Decorator
 
@@ -307,15 +310,24 @@
       message: Message to display if there are problems with the OAuth 2.0
         configuration. The message may contain HTML and will be presented on the
         web interface for any method that uses the decorator.
+      callback_path: string, The absolute path to use as the callback URI. Note
+        that this must match up with the URI given when registering the
+        application in the APIs Console.
       **kwargs: dict, Keyword arguments are be passed along as kwargs to the
         OAuth2WebServerFlow constructor.
     """
-    self.flow = OAuth2WebServerFlow(client_id, client_secret, scope, user_agent,
-        auth_uri, token_uri, **kwargs)
+    self.flow = None
     self.credentials = None
-    self._request_handler = None
+    self._client_id = client_id
+    self._client_secret = client_secret
+    self._scope = scope
+    self._auth_uri = auth_uri
+    self._token_uri = token_uri
+    self._user_agent = user_agent
+    self._kwargs = kwargs
     self._message = message
     self._in_error = False
+    self._callback_path = callback_path
 
   def _display_error_message(self, request_handler):
     request_handler.response.out.write('<html><body>')
@@ -344,9 +356,11 @@
         request_handler.redirect(users.create_login_url(
             request_handler.request.uri))
         return
+
+      self._create_flow(request_handler)
+
       # Store the request URI in 'state' so we can use it later
       self.flow.params['state'] = request_handler.request.url
-      self._request_handler = request_handler
       self.credentials = StorageByKeyName(
           CredentialsModel, user.user_id(), 'credentials').get()
 
@@ -359,6 +373,26 @@
 
     return check_oauth
 
+  def _create_flow(self, request_handler):
+    """Create the Flow object.
+
+    The Flow is calculated lazily since we don't know where this app is
+    running until it receives a request, at which point redirect_uri can be
+    calculated and then the Flow object can be constructed.
+
+    Args:
+      request_handler: webapp.RequestHandler, the request handler.
+    """
+    if self.flow is None:
+      redirect_uri = request_handler.request.relative_url(
+          self._callback_path) # Usually /oauth2callback
+      self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
+                                      self._scope, redirect_uri=redirect_uri,
+                                      user_agent=self._user_agent,
+                                      auth_uri=self._auth_uri,
+                                      token_uri=self._token_uri, **self._kwargs)
+
+
   def oauth_aware(self, method):
     """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
 
@@ -385,9 +419,9 @@
             request_handler.request.uri))
         return
 
+      self._create_flow(request_handler)
 
       self.flow.params['state'] = request_handler.request.url
-      self._request_handler = request_handler
       self.credentials = StorageByKeyName(
           CredentialsModel, user.user_id(), 'credentials').get()
       method(request_handler, *args, **kwargs)
@@ -407,11 +441,7 @@
     Must only be called from with a webapp.RequestHandler subclassed method
     that had been decorated with either @oauth_required or @oauth_aware.
     """
-    callback = self._request_handler.request.relative_url('/oauth2callback')
-    url = self.flow.step1_get_authorize_url(callback)
-    user = users.get_current_user()
-    memcache.set(user.user_id(), pickle.dumps(self.flow),
-                 namespace=OAUTH2CLIENT_NAMESPACE)
+    url = self.flow.step1_get_authorize_url()
     return str(url)
 
   def http(self):
@@ -423,6 +453,70 @@
     """
     return self.credentials.authorize(httplib2.Http())
 
+  @property
+  def callback_path(self):
+    """The absolute path where the callback will occur.
+
+    Note this is the absolute path, not the absolute URI, that will be
+    calculated by the decorator at runtime. See callback_handler() for how this
+    should be used.
+
+    Returns:
+      The callback path as a string.
+    """
+    return self._callback_path
+
+
+  def callback_handler(self):
+    """RequestHandler for the OAuth 2.0 redirect callback.
+
+    Usage:
+       app = webapp.WSGIApplication([
+         ('/index', MyIndexHandler),
+         ...,
+         (decorator.callback_path, decorator.callback_handler())
+       ])
+
+    Returns:
+      A webapp.RequestHandler that handles the redirect back from the
+      server during the OAuth 2.0 dance.
+    """
+    decorator = self
+
+    class OAuth2Handler(webapp.RequestHandler):
+      """Handler for the redirect_uri of the OAuth 2.0 dance."""
+
+      @login_required
+      def get(self):
+        error = self.request.get('error')
+        if error:
+          errormsg = self.request.get('error_description', error)
+          self.response.out.write(
+              'The authorization request failed: %s' % errormsg)
+        else:
+          user = users.get_current_user()
+          decorator._create_flow(self)
+          credentials = decorator.flow.step2_exchange(self.request.params)
+          StorageByKeyName(
+              CredentialsModel, user.user_id(), 'credentials').put(credentials)
+          self.redirect(str(self.request.get('state')))
+
+    return OAuth2Handler
+
+  def callback_application(self):
+    """WSGI application for handling the OAuth 2.0 redirect callback.
+
+    If you need finer grained control use `callback_handler` which returns just
+    the webapp.RequestHandler.
+
+    Returns:
+      A webapp.WSGIApplication that handles the redirect back from the
+      server during the OAuth 2.0 dance.
+    """
+    return webapp.WSGIApplication([
+        (self.callback_path, self.callback_handler())
+        ])
+
 
 class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
   """An OAuth2Decorator that builds from a clientsecrets file.
@@ -446,6 +540,7 @@
         # in API calls
   """
 
+  @util.positional(3)
   def __init__(self, filename, scope, message=None, cache=None):
     """Constructor
 
@@ -457,7 +552,7 @@
         clientsecrets file is missing or invalid. The message may contain HTML and
         will be presented on the web interface for any method that uses the
         decorator.
-      cache: An optional cache service client that implements get() and set() 
+      cache: An optional cache service client that implements get() and set()
         methods. See clientsecrets.loadfile() for details.
     """
     try:
@@ -469,9 +564,9 @@
                 client_info['client_id'],
                 client_info['client_secret'],
                 scope,
-                client_info['auth_uri'],
-                client_info['token_uri'],
-                message)
+                auth_uri=client_info['auth_uri'],
+                token_uri=client_info['token_uri'],
+                message=message)
     except clientsecrets.InvalidClientSecretsError:
       self._in_error = True
     if message is not None:
@@ -480,7 +575,8 @@
       self._message = "Please configure your application for OAuth 2.0"
 
 
-def oauth2decorator_from_clientsecrets(filename, scope, 
+@util.positional(2)
+def oauth2decorator_from_clientsecrets(filename, scope,
                                        message=None, cache=None):
   """Creates an OAuth2Decorator populated from a clientsecrets file.
 
@@ -492,46 +588,11 @@
       clientsecrets file is missing or invalid. The message may contain HTML and
       will be presented on the web interface for any method that uses the
       decorator.
-    cache: An optional cache service client that implements get() and set() 
+    cache: An optional cache service client that implements get() and set()
       methods. See clientsecrets.loadfile() for details.
 
   Returns: An OAuth2Decorator
 
   """
-  return OAuth2DecoratorFromClientSecrets(filename, scope, 
+  return OAuth2DecoratorFromClientSecrets(filename, scope,
     message=message, cache=cache)
-
-
-class OAuth2Handler(webapp.RequestHandler):
-  """Handler for the redirect_uri of the OAuth 2.0 dance."""
-
-  @login_required
-  def get(self):
-    error = self.request.get('error')
-    if error:
-      errormsg = self.request.get('error_description', error)
-      self.response.out.write(
-          'The authorization request failed: %s' % errormsg)
-    else:
-      user = users.get_current_user()
-      flow = pickle.loads(memcache.get(user.user_id(),
-                                       namespace=OAUTH2CLIENT_NAMESPACE))
-      # This code should be ammended with application specific error
-      # handling. The following cases should be considered:
-      # 1. What if the flow doesn't exist in memcache? Or is corrupt?
-      # 2. What if the step2_exchange fails?
-      if flow:
-        credentials = flow.step2_exchange(self.request.params)
-        StorageByKeyName(
-            CredentialsModel, user.user_id(), 'credentials').put(credentials)
-        self.redirect(str(self.request.get('state')))
-      else:
-        # TODO Add error handling here.
-        pass
-
-
-application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
-
-
-def main():
-  run_wsgi_app(application)
diff --git a/oauth2client/client.py b/oauth2client/client.py
index 4e7ffcb..6b1bdbd 100644
--- a/oauth2client/client.py
+++ b/oauth2client/client.py
@@ -31,7 +31,8 @@
 import urllib
 import urlparse
 
-from anyjson import simplejson
+from oauth2client import util
+from oauth2client.anyjson import simplejson
 
 HAS_OPENSSL = False
 try:
@@ -327,6 +328,7 @@
   OAuth2Credentials objects may be safely pickled and unpickled.
   """
 
+  @util.positional(8)
   def __init__(self, access_token, client_id, client_secret, refresh_token,
                token_expiry, token_uri, user_agent, id_token=None):
     """Create an instance of OAuth2Credentials.
@@ -394,6 +396,7 @@
     request_orig = http.request
 
     # The closure that will replace 'httplib2.Http.request'.
+    @util.positional(1)
     def new_request(uri, method='GET', body=None, headers=None,
                     redirections=httplib2.DEFAULT_MAX_REDIRECTS,
                     connection_type=None):
@@ -481,7 +484,7 @@
         data['token_expiry'],
         data['token_uri'],
         data['user_agent'],
-        data.get('id_token', None))
+        id_token=data.get('id_token', None))
     retval.invalid = data['invalid']
     return retval
 
@@ -699,7 +702,8 @@
   AssertionCredentials objects may be safely pickled and unpickled.
   """
 
-  def __init__(self, assertion_type, user_agent,
+  @util.positional(2)
+  def __init__(self, assertion_type, user_agent=None,
                token_uri='https://accounts.google.com/o/oauth2/token',
                **unused_kwargs):
     """Constructor for AssertionFlowCredentials.
@@ -757,6 +761,7 @@
 
     MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
 
+    @util.positional(4)
     def __init__(self,
         service_account_name,
         private_key,
@@ -781,7 +786,7 @@
 
       super(SignedJwtAssertionCredentials, self).__init__(
           'http://oauth.net/grant_type/jwt/1.0/bearer',
-          user_agent,
+          user_agent=user_agent,
           token_uri=token_uri,
           )
 
@@ -833,6 +838,7 @@
   # for the certs.
   _cached_http = httplib2.Http(MemoryCache())
 
+  @util.positional(2)
   def verify_id_token(id_token, audience, http=None,
       cert_uri=ID_TOKEN_VERIFICATON_CERTS):
     """Verifies a signed JWT id_token.
@@ -892,6 +898,7 @@
 
   return simplejson.loads(_urlsafe_b64decode(segments[1]))
 
+
 def _parse_exchange_token_response(content):
   """Parses response of an exchange token request.
 
@@ -919,10 +926,11 @@
 
   return resp
 
+
+@util.positional(4)
 def credentials_from_code(client_id, client_secret, scope, code,
-                        redirect_uri = 'postmessage',
-                        http=None, user_agent=None,
-                        token_uri='https://accounts.google.com/o/oauth2/token'):
+    redirect_uri='postmessage', http=None, user_agent=None,
+    token_uri='https://accounts.google.com/o/oauth2/token'):
   """Exchanges an authorization code for an OAuth2Credentials object.
 
   Args:
@@ -943,19 +951,19 @@
     FlowExchangeError if the authorization code cannot be exchanged for an
      access token
   """
-  flow = OAuth2WebServerFlow(client_id, client_secret, scope, user_agent,
-                             'https://accounts.google.com/o/oauth2/auth',
-                             token_uri)
+  flow = OAuth2WebServerFlow(client_id, client_secret, scope,
+                             redirect_uri=redirect_uri, user_agent=user_agent,
+                             auth_uri='https://accounts.google.com/o/oauth2/auth',
+                             token_uri=token_uri)
 
-  # We primarily make this call to set up the redirect_uri in the flow object
-  uriThatWeDontReallyUse = flow.step1_get_authorize_url(redirect_uri)
-  credentials = flow.step2_exchange(code, http)
+  credentials = flow.step2_exchange(code, http=http)
   return credentials
 
 
+@util.positional(3)
 def credentials_from_clientsecrets_and_code(filename, scope, code,
                                             message = None,
-                                            redirect_uri = 'postmessage',
+                                            redirect_uri='postmessage',
                                             http=None,
                                             cache=None):
   """Returns OAuth2Credentials from a clientsecrets file and an auth code.
@@ -966,7 +974,7 @@
   Args:
     filename: string, File name of clientsecrets.
     scope: string or list of strings, scope(s) to request.
-    code: string, An authroization code, most likely passed down from
+    code: string, An authorization code, most likely passed down from
       the client
     message: string, A friendly string to display to the user if the
       clientsecrets file is missing or invalid. If message is provided then
@@ -975,7 +983,7 @@
     redirect_uri: string, this is generally set to 'postmessage' to match the
       redirect_uri that the client specified
     http: httplib2.Http, optional http instance to use to do the fetch
-    cache: An optional cache service client that implements get() and set() 
+    cache: An optional cache service client that implements get() and set()
       methods. See clientsecrets.loadfile() for details.
 
   Returns:
@@ -988,20 +996,22 @@
     clientsecrets.InvalidClientSecretsError if the clientsecrets file is
       invalid.
   """
-  flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache)
-  # We primarily make this call to set up the redirect_uri in the flow object
-  uriThatWeDontReallyUse = flow.step1_get_authorize_url(redirect_uri)
-  credentials = flow.step2_exchange(code, http)
+  flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache,
+                                 redirect_uri=redirect_uri)
+  credentials = flow.step2_exchange(code, http=http)
   return credentials
 
 
 class OAuth2WebServerFlow(Flow):
   """Does the Web Server Flow for OAuth 2.0.
 
-  OAuth2Credentials objects may be safely pickled and unpickled.
+  OAuth2WebServerFlow objects may be safely pickled and unpickled.
   """
 
-  def __init__(self, client_id, client_secret, scope, user_agent=None,
+  @util.positional(4)
+  def __init__(self, client_id, client_secret, scope,
+               redirect_uri=None,
+               user_agent=None,
                auth_uri='https://accounts.google.com/o/oauth2/auth',
                token_uri='https://accounts.google.com/o/oauth2/token',
                **kwargs):
@@ -1012,6 +1022,9 @@
       client_secret: string client secret.
       scope: string or list of strings, scope(s) of the credentials being
         requested.
+      redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
+          a non-web-based application, or a URI that handles the callback from
+          the authorization server.
       user_agent: string, HTTP User-Agent to provide for this application.
       auth_uri: string, URI for authorization endpoint. For convenience
         defaults to Google's endpoints but any OAuth 2.0 provider can be used.
@@ -1025,6 +1038,7 @@
     if type(scope) is list:
       scope = ' '.join(scope)
     self.scope = scope
+    self.redirect_uri = redirect_uri
     self.user_agent = user_agent
     self.auth_uri = auth_uri
     self.token_uri = token_uri
@@ -1032,27 +1046,33 @@
         'access_type': 'offline',
         }
     self.params.update(kwargs)
-    self.redirect_uri = None
 
-  def step1_get_authorize_url(self, redirect_uri=OOB_CALLBACK_URN):
+  @util.positional(1)
+  def step1_get_authorize_url(self, redirect_uri=None):
     """Returns a URI to redirect to the provider.
 
     Args:
       redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
           a non-web-based application, or a URI that handles the callback from
-          the authorization server.
+          the authorization server. This parameter is deprecated, please move to
+          passing the redirect_uri in via the constructor.
 
-    If redirect_uri is 'urn:ietf:wg:oauth:2.0:oob' then pass in the
-    generated verification code to step2_exchange,
-    otherwise pass in the query parameters received
-    at the callback uri to step2_exchange.
+    Returns:
+      A URI as a string to redirect the user to begin the authorization flow.
     """
+    if redirect_uri is not None:
+      logger.warning(('The redirect_uri parameter for'
+          'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please'
+          'move to passing the redirect_uri in via the constructor.'))
+      self.redirect_uri = redirect_uri
 
-    self.redirect_uri = redirect_uri
+    if self.redirect_uri is None:
+      raise ValueError('The value of redirect_uri must not be None.')
+
     query = {
         'response_type': 'code',
         'client_id': self.client_id,
-        'redirect_uri': redirect_uri,
+        'redirect_uri': self.redirect_uri,
         'scope': self.scope,
         }
     query.update(self.params)
@@ -1061,6 +1081,7 @@
     parts[4] = urllib.urlencode(query)
     return urlparse.urlunparse(parts)
 
+  @util.positional(2)
   def step2_exchange(self, code, http=None):
     """Exhanges a code for OAuth2Credentials.
 
@@ -1134,7 +1155,9 @@
         error_msg = 'Invalid response: %s.' % str(resp.status)
       raise FlowExchangeError(error_msg)
 
-def flow_from_clientsecrets(filename, scope, message=None, cache=None):
+
+@util.positional(2)
+def flow_from_clientsecrets(filename, scope, redirect_uri=None, message=None, cache=None):
   """Create a Flow from a clientsecrets file.
 
   Will create the right kind of Flow based on the contents of the clientsecrets
@@ -1143,11 +1166,14 @@
   Args:
     filename: string, File name of client secrets.
     scope: string or list of strings, scope(s) to request.
+    redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
+        a non-web-based application, or a URI that handles the callback from
+        the authorization server.
     message: string, A friendly string to display to the user if the
       clientsecrets file is missing or invalid. If message is provided then
       sys.exit will be called in the case of an error. If message in not
       provided then clientsecrets.InvalidClientSecretsError will be raised.
-    cache: An optional cache service client that implements get() and set() 
+    cache: An optional cache service client that implements get() and set()
       methods. See clientsecrets.loadfile() for details.
 
   Returns:
@@ -1165,9 +1191,11 @@
             client_info['client_id'],
             client_info['client_secret'],
             scope,
-            None, # user_agent
-            client_info['auth_uri'],
-            client_info['token_uri'])
+            redirect_uri=redirect_uri,
+            user_agent=None,
+            auth_uri=client_info['auth_uri'],
+            token_uri=client_info['token_uri'])
+
   except clientsecrets.InvalidClientSecretsError:
     if message:
       sys.exit(message)
diff --git a/oauth2client/locked_file.py b/oauth2client/locked_file.py
index 8a7aff5..8f35c90 100644
--- a/oauth2client/locked_file.py
+++ b/oauth2client/locked_file.py
@@ -23,6 +23,8 @@
 import os
 import time
 
+from oauth2client import util
+
 logger = logging.getLogger(__name__)
 
 
@@ -292,6 +294,7 @@
 class LockedFile(object):
   """Represent a file that has exclusive access."""
 
+  @util.positional(4)
   def __init__(self, filename, mode, fallback_mode, use_native_locking=True):
     """Construct a LockedFile.
 
diff --git a/oauth2client/multistore_file.py b/oauth2client/multistore_file.py
index 60ac684..e190c6a 100644
--- a/oauth2client/multistore_file.py
+++ b/oauth2client/multistore_file.py
@@ -38,8 +38,9 @@
 import threading
 
 from anyjson import simplejson
-from client import Storage as BaseStorage
-from client import Credentials
+from oauth2client.client import Storage as BaseStorage
+from oauth2client.client import Credentials
+from oauth2client import util
 from locked_file import LockedFile
 
 logger = logging.getLogger(__name__)
@@ -59,6 +60,7 @@
   pass
 
 
+@util.positional(4)
 def get_credential_storage(filename, client_id, user_agent, scope,
                            warn_on_readonly=True):
   """Get a Storage instance for a credential.
@@ -78,7 +80,7 @@
   _multistores_lock.acquire()
   try:
     multistore = _multistores.setdefault(
-        filename, _MultiStore(filename, warn_on_readonly))
+        filename, _MultiStore(filename, warn_on_readonly=warn_on_readonly))
   finally:
     _multistores_lock.release()
   if type(scope) is list:
@@ -89,6 +91,7 @@
 class _MultiStore(object):
   """A file backed store for multiple credentials."""
 
+  @util.positional(2)
   def __init__(self, filename, warn_on_readonly=True):
     """Initialize the class.
 
diff --git a/oauth2client/tools.py b/oauth2client/tools.py
index 5d96ea4..1faa9ff 100644
--- a/oauth2client/tools.py
+++ b/oauth2client/tools.py
@@ -29,8 +29,9 @@
 import sys
 import webbrowser
 
-from client import FlowExchangeError
-from client import OOB_CALLBACK_URN
+from oauth2client.client import FlowExchangeError
+from oauth2client.client import OOB_CALLBACK_URN
+from oauth2client import util
 
 try:
   from urlparse import parse_qsl
@@ -91,6 +92,7 @@
     pass
 
 
+@util.positional(2)
 def run(flow, storage, http=None):
   """Core code for a command-line application.
 
@@ -130,7 +132,8 @@
     oauth_callback = 'http://%s:%s/' % (FLAGS.auth_host_name, port_number)
   else:
     oauth_callback = OOB_CALLBACK_URN
-  authorize_url = flow.step1_get_authorize_url(oauth_callback)
+  flow.redirect_uri = oauth_callback
+  authorize_url = flow.step1_get_authorize_url()
 
   if FLAGS.auth_local_webserver:
     webbrowser.open(authorize_url, new=1, autoraise=True)
@@ -163,7 +166,7 @@
     code = raw_input('Enter verification code: ').strip()
 
   try:
-    credential = flow.step2_exchange(code, http)
+    credential = flow.step2_exchange(code, http=http)
   except FlowExchangeError, e:
     sys.exit('Authentication has failed: %s' % e)
 
diff --git a/oauth2client/util.py b/oauth2client/util.py
new file mode 100644
index 0000000..bda14c6
--- /dev/null
+++ b/oauth2client/util.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python
+#
+# Copyright 2010 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Common utility library."""
+
+__author__ = ['rafek@google.com (Rafe Kaplan)',
+              'guido@google.com (Guido van Rossum)',
+]
+__all__ = [
+  'positional',
+]
+
+import gflags
+import inspect
+import logging
+
+logger = logging.getLogger(__name__)
+
+FLAGS = gflags.FLAGS
+
+gflags.DEFINE_enum('positional_parameters_enforcement', 'WARNING',
+    ['EXCEPTION', 'WARNING', 'IGNORE'],
+    'The action when an oauth2client.util.positional declaration is violated.')
+
+
+def positional(max_positional_args):
+  """A decorator to declare that only the first N arguments my be positional.
+
+  This decorator makes it easy to support Python 3 style key-word only
+  parameters.  For example, in Python 3 it is possible to write:
+
+    def fn(pos1, *, kwonly1=None, kwonly1=None):
+      ...
+
+  All named parameters after * must be a keyword:
+
+    fn(10, 'kw1', 'kw2')  # Raises exception.
+    fn(10, kwonly1='kw1')  # Ok.
+
+  Example:
+    To define a function like above, do:
+
+      @positional(1)
+      def fn(pos1, kwonly1=None, kwonly2=None):
+        ...
+
+    If no default value is provided to a keyword argument, it becomes a required
+    keyword argument:
+
+      @positional(0)
+      def fn(required_kw):
+        ...
+
+    This must be called with the keyword parameter:
+
+      fn()  # Raises exception.
+      fn(10)  # Raises exception.
+      fn(required_kw=10)  # Ok.
+
+    When defining instance or class methods always remember to account for
+    'self' and 'cls':
+
+      class MyClass(object):
+
+        @positional(2)
+        def my_method(self, pos1, kwonly1=None):
+          ...
+
+        @classmethod
+        @positional(2)
+        def my_method(cls, pos1, kwonly1=None):
+          ...
+
+  The positional decorator behavior is controlled by the
+  --positional_parameters_enforcement flag. The flag may be set to 'EXCEPTION',
+  'WARNING' or 'IGNORE' to raise an exception, log a warning, or do nothing,
+  respectively, if a declaration is violated.
+
+  Args:
+    max_positional_arguments: Maximum number of positional arguments.  All
+      parameters after the this index must be keyword only.
+
+  Returns:
+    A decorator that prevents using arguments after max_positional_args from
+    being used as positional parameters.
+
+  Raises:
+    TypeError if a key-word only argument is provided as a positional parameter,
+    but only if the --positional_parameters_enforcement flag is set to
+    'EXCEPTION'.
+  """
+  def positional_decorator(wrapped):
+    def positional_wrapper(*args, **kwargs):
+      if len(args) > max_positional_args:
+        plural_s = ''
+        if max_positional_args != 1:
+          plural_s = 's'
+        message = '%s() takes at most %d positional argument%s (%d given)' % (
+            wrapped.__name__, max_positional_args, plural_s, len(args))
+        if FLAGS.positional_parameters_enforcement == 'EXCEPTION':
+          raise TypeError(message)
+        elif FLAGS.positional_parameters_enforcement == 'WARNING':
+          logger.warning(message)
+        else: # IGNORE
+          pass
+      return wrapped(*args, **kwargs)
+    return positional_wrapper
+
+  if isinstance(max_positional_args, (int, long)):
+    return positional_decorator
+  else:
+    args, _, _, defaults = inspect.getargspec(max_positional_args)
+    return positional(len(args) - len(defaults))(max_positional_args)