Skip to content
This repository has been archived by the owner on Nov 5, 2019. It is now read-only.

Merge util.py and _helpers.py #579

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion docs/source/oauth2client.rst
Expand Up @@ -20,7 +20,6 @@ Submodules
oauth2client.service_account
oauth2client.tools
oauth2client.transport
oauth2client.util

Module contents
---------------
Expand Down
7 changes: 0 additions & 7 deletions docs/source/oauth2client.util.rst

This file was deleted.

197 changes: 197 additions & 0 deletions oauth2client/_helpers.py
Expand Up @@ -11,12 +11,209 @@
# 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.

"""Helper functions for commonly used utilities."""

import base64
import functools
import inspect
import json
import logging
import os
import warnings

import six
from six.moves import urllib


__author__ = (
'rafek@google.com (Rafe Kaplan)',
'guido@google.com (Guido van Rossum)',
)

logger = logging.getLogger(__name__)

POSITIONAL_WARNING = 'WARNING'
POSITIONAL_EXCEPTION = 'EXCEPTION'
POSITIONAL_IGNORE = 'IGNORE'
POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION,
POSITIONAL_IGNORE])

positional_parameters_enforcement = POSITIONAL_WARNING

_SYM_LINK_MESSAGE = 'File: {0}: Is a symbolic link.'
_IS_DIR_MESSAGE = '{0}: Is a directory'
_MISSING_FILE_MESSAGE = 'Cannot access {0}: No such file or directory'


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 keyword-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
``_helpers.positional_parameters_enforcement``, which may be set to
``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or
``POSITIONAL_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
_helpers.positional_parameters_enforcement is set to
POSITIONAL_EXCEPTION.
"""

def positional_decorator(wrapped):
@functools.wraps(wrapped)
def positional_wrapper(*args, **kwargs):
if len(args) > max_positional_args:
plural_s = ''
if max_positional_args != 1:
plural_s = 's'
message = ('{function}() takes at most {args_max} positional '
'argument{plural} ({args_given} given)'.format(
function=wrapped.__name__,
args_max=max_positional_args,
args_given=len(args),
plural=plural_s))
if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
raise TypeError(message)
elif positional_parameters_enforcement == POSITIONAL_WARNING:
logger.warning(message)
return wrapped(*args, **kwargs)
return positional_wrapper

if isinstance(max_positional_args, six.integer_types):
return positional_decorator
else:
args, _, _, defaults = inspect.getargspec(max_positional_args)
return positional(len(args) - len(defaults))(max_positional_args)


def scopes_to_string(scopes):
"""Converts scope value to a string.

If scopes is a string then it is simply passed through. If scopes is an
iterable then a string is returned that is all the individual scopes
concatenated with spaces.

Args:
scopes: string or iterable of strings, the scopes.

Returns:
The scopes formatted as a single string.
"""
if isinstance(scopes, six.string_types):
return scopes
else:
return ' '.join(scopes)


def string_to_scopes(scopes):
"""Converts stringifed scope value to a list.

If scopes is a list then it is simply passed through. If scopes is an
string then a list of each individual scope is returned.

Args:
scopes: a string or iterable of strings, the scopes.

Returns:
The scopes in a list.
"""
if not scopes:
return []
elif isinstance(scopes, six.string_types):
return scopes.split(' ')
else:
return scopes


def _add_query_parameter(url, name, value):

This comment was marked as spam.

This comment was marked as spam.

"""Adds a query parameter to a url.

Replaces the current value if it already exists in the URL.

Args:
url: string, url to add the query parameter to.
name: string, query parameter name.
value: string, query parameter value.

Returns:
Updated query parameter. Does not update the url if value is None.
"""
if value is None:
return url
else:
parsed = list(urllib.parse.urlparse(url))
query = dict(urllib.parse.parse_qsl(parsed[4]))
query[name] = value
parsed[4] = urllib.parse.urlencode(query)
return urllib.parse.urlunparse(parsed)


def validate_file(filename):
if os.path.islink(filename):
raise IOError(_SYM_LINK_MESSAGE.format(filename))
elif os.path.isdir(filename):
raise IOError(_IS_DIR_MESSAGE.format(filename))
elif not os.path.isfile(filename):
warnings.warn(_MISSING_FILE_MESSAGE.format(filename))


def _parse_pem_key(raw_key_input):
Expand Down
29 changes: 14 additions & 15 deletions oauth2client/client.py
Expand Up @@ -36,7 +36,6 @@
from oauth2client import _helpers
from oauth2client import clientsecrets
from oauth2client import transport
from oauth2client import util


__author__ = 'jcgregorio@google.com (Joe Gregorio)'
Expand Down Expand Up @@ -466,7 +465,7 @@ class OAuth2Credentials(Credentials):
OAuth2Credentials objects may be safely pickled and unpickled.
"""

@util.positional(8)
@_helpers.positional(8)
def __init__(self, access_token, client_id, client_secret, refresh_token,
token_expiry, token_uri, user_agent, revoke_uri=None,
id_token=None, token_response=None, scopes=None,
Expand Down Expand Up @@ -513,7 +512,7 @@ def __init__(self, access_token, client_id, client_secret, refresh_token,
self.revoke_uri = revoke_uri
self.id_token = id_token
self.token_response = token_response
self.scopes = set(util.string_to_scopes(scopes or []))
self.scopes = set(_helpers.string_to_scopes(scopes or []))
self.token_info_uri = token_info_uri

# True if the credentials have been revoked or expired and can't be
Expand Down Expand Up @@ -592,7 +591,7 @@ def has_scopes(self, scopes):
not have scopes. In both cases, you can use refresh_scopes() to
obtain the canonical set of scopes.
"""
scopes = util.string_to_scopes(scopes)
scopes = _helpers.string_to_scopes(scopes)
return set(scopes).issubset(self.scopes)

def retrieve_scopes(self, http):
Expand Down Expand Up @@ -908,7 +907,7 @@ def _do_retrieve_scopes(self, http_request, token):
content = _helpers._from_bytes(content)
if resp.status == http_client.OK:
d = json.loads(content)
self.scopes = set(util.string_to_scopes(d.get('scope', '')))
self.scopes = set(_helpers.string_to_scopes(d.get('scope', '')))
else:
error_msg = 'Invalid response {0}.'.format(resp.status)
try:
Expand Down Expand Up @@ -1469,7 +1468,7 @@ class AssertionCredentials(GoogleCredentials):
AssertionCredentials objects may be safely pickled and unpickled.
"""

@util.positional(2)
@_helpers.positional(2)
def __init__(self, assertion_type, user_agent=None,
token_uri=oauth2client.GOOGLE_TOKEN_URI,
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
Expand Down Expand Up @@ -1545,7 +1544,7 @@ def _require_crypto_or_die():
raise CryptoUnavailableError('No crypto library available')


@util.positional(2)
@_helpers.positional(2)
def verify_id_token(id_token, audience, http=None,
cert_uri=ID_TOKEN_VERIFICATION_CERTS):
"""Verifies a signed JWT id_token.
Expand Down Expand Up @@ -1633,7 +1632,7 @@ def _parse_exchange_token_response(content):
return resp


@util.positional(4)
@_helpers.positional(4)
def credentials_from_code(client_id, client_secret, scope, code,
redirect_uri='postmessage', http=None,
user_agent=None,
Expand Down Expand Up @@ -1684,7 +1683,7 @@ def credentials_from_code(client_id, client_secret, scope, code,
return credentials


@util.positional(3)
@_helpers.positional(3)
def credentials_from_clientsecrets_and_code(filename, scope, code,
message=None,
redirect_uri='postmessage',
Expand Down Expand Up @@ -1803,7 +1802,7 @@ class OAuth2WebServerFlow(Flow):
OAuth2WebServerFlow objects may be safely pickled and unpickled.
"""

@util.positional(4)
@_helpers.positional(4)
def __init__(self, client_id,
client_secret=None,
scope=None,
Expand Down Expand Up @@ -1862,7 +1861,7 @@ def __init__(self, client_id,
raise TypeError("The value of scope must not be None")
self.client_id = client_id
self.client_secret = client_secret
self.scope = util.scopes_to_string(scope)
self.scope = _helpers.scopes_to_string(scope)
self.redirect_uri = redirect_uri
self.login_hint = login_hint
self.user_agent = user_agent
Expand All @@ -1874,7 +1873,7 @@ def __init__(self, client_id,
self.authorization_header = authorization_header
self.params = _oauth2_web_server_flow_params(kwargs)

@util.positional(1)
@_helpers.positional(1)
def step1_get_authorize_url(self, redirect_uri=None, state=None):
"""Returns a URI to redirect to the provider.

Expand Down Expand Up @@ -1915,7 +1914,7 @@ def step1_get_authorize_url(self, redirect_uri=None, state=None):
query_params.update(self.params)
return _update_query_params(self.auth_uri, query_params)

@util.positional(1)
@_helpers.positional(1)
def step1_get_device_and_user_codes(self, http=None):
"""Returns a user code and the verification URL where to enter it

Expand Down Expand Up @@ -1963,7 +1962,7 @@ def step1_get_device_and_user_codes(self, http=None):
pass
raise OAuth2DeviceCodeError(error_msg)

@util.positional(2)
@_helpers.positional(2)
def step2_exchange(self, code=None, http=None, device_flow_info=None):
"""Exchanges a code for OAuth2Credentials.

Expand Down Expand Up @@ -2060,7 +2059,7 @@ def step2_exchange(self, code=None, http=None, device_flow_info=None):
raise FlowExchangeError(error_msg)


@util.positional(2)
@_helpers.positional(2)
def flow_from_clientsecrets(filename, scope, redirect_uri=None,
message=None, cache=None, login_hint=None,
device_uri=None):
Expand Down
3 changes: 1 addition & 2 deletions oauth2client/contrib/_metadata.py
Expand Up @@ -25,7 +25,6 @@

from oauth2client import _helpers
from oauth2client import client
from oauth2client import util


METADATA_ROOT = 'http://metadata.google.internal/computeMetadata/v1/'
Expand Down Expand Up @@ -54,7 +53,7 @@ def get(http_request, path, root=METADATA_ROOT, recursive=None):
retrieving metadata.
"""
url = urlparse.urljoin(root, path)
url = util._add_query_parameter(url, 'recursive', recursive)
url = _helpers._add_query_parameter(url, 'recursive', recursive)

response, content = http_request(
url,
Expand Down