Skip to content

Commit 669bad2

Browse files
committed
Merge branch 'python3' into merge
2 parents a12a2c8 + aa573ee commit 669bad2

26 files changed

+361
-234
lines changed

.travis.yml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
language: python
22
python: 2.7
33
env:
4-
- TOX_ENV=py26
5-
- TOX_ENV=py27
6-
- TOX_ENV=pypy
4+
- TOX_ENV=py26openssl13
5+
- TOX_ENV=py26openssl14
6+
- TOX_ENV=py27openssl13
7+
- TOX_ENV=py27openssl14
8+
- TOX_ENV=py33openssl14
9+
- TOX_ENV=py34openssl14
10+
- TOX_ENV=pypyopenssl13
11+
- TOX_ENV=pypyopenssl14
712
install:
813
- pip install tox
9-
- pip install .
1014
script:
1115
- tox -e $TOX_ENV
1216
notifications:

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
[![Build Status](https://travis-ci.org/google/oauth2client.svg?branch=master)](https://travis-ci.org/google/oauth2client)
22

3+
NOTE
4+
====
5+
6+
This is a work-in-progress branch to add python3 support to oauth2client. Most
7+
of the work was done by @pferate.
8+
39
This is a client library for accessing resources protected by OAuth 2.0.
410

511
[Full documentation](http://google.github.io/oauth2client/)

oauth2client/client.py

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
import os
2929
import sys
3030
import time
31-
import urllib
32-
import urlparse
31+
import six
32+
from six.moves import urllib
3333

3434
import httplib2
3535
from oauth2client import clientsecrets
@@ -231,6 +231,9 @@ def _to_json(self, strip):
231231
# Add in information we will need later to reconsistitue this instance.
232232
d['_class'] = t.__name__
233233
d['_module'] = t.__module__
234+
for key, val in d.items():
235+
if isinstance(val, bytes):
236+
d[key] = val.decode('utf-8')
234237
return json.dumps(d)
235238

236239
def to_json(self):
@@ -254,6 +257,8 @@ def new_from_json(cls, s):
254257
An instance of the subclass of Credentials that was serialized with
255258
to_json().
256259
"""
260+
if six.PY3 and isinstance(s, bytes):
261+
s = s.decode('utf-8')
257262
data = json.loads(s)
258263
# Find and call the right classmethod from_json() to restore the object.
259264
module = data['_module']
@@ -398,7 +403,7 @@ def clean_headers(headers):
398403
"""
399404
clean = {}
400405
try:
401-
for k, v in headers.iteritems():
406+
for k, v in six.iteritems(headers):
402407
clean[str(k)] = str(v)
403408
except UnicodeEncodeError:
404409
raise NonAsciiHeaderError(k + ': ' + v)
@@ -415,11 +420,11 @@ def _update_query_params(uri, params):
415420
Returns:
416421
The same URI but with the new query parameters added.
417422
"""
418-
parts = urlparse.urlparse(uri)
419-
query_params = dict(urlparse.parse_qsl(parts.query))
423+
parts = urllib.parse.urlparse(uri)
424+
query_params = dict(urllib.parse.parse_qsl(parts.query))
420425
query_params.update(params)
421-
new_parts = parts._replace(query=urllib.urlencode(query_params))
422-
return urlparse.urlunparse(new_parts)
426+
new_parts = parts._replace(query=urllib.parse.urlencode(query_params))
427+
return urllib.parse.urlunparse(new_parts)
423428

424429

425430
class OAuth2Credentials(Credentials):
@@ -589,6 +594,8 @@ def from_json(cls, s):
589594
Returns:
590595
An instance of a Credentials subclass.
591596
"""
597+
if six.PY3 and isinstance(s, bytes):
598+
s = s.decode('utf-8')
592599
data = json.loads(s)
593600
if (data.get('token_expiry') and
594601
not isinstance(data['token_expiry'], datetime.datetime)):
@@ -691,7 +698,7 @@ def __setstate__(self, state):
691698

692699
def _generate_refresh_request_body(self):
693700
"""Generate the body that will be used in the refresh request."""
694-
body = urllib.urlencode({
701+
body = urllib.parse.urlencode({
695702
'grant_type': 'refresh_token',
696703
'client_id': self.client_id,
697704
'client_secret': self.client_secret,
@@ -755,8 +762,9 @@ def _do_refresh_request(self, http_request):
755762
logger.info('Refreshing access_token')
756763
resp, content = http_request(
757764
self.token_uri, method='POST', body=body, headers=headers)
765+
if six.PY3:
766+
content = content.decode('utf-8')
758767
if resp.status == 200:
759-
# TODO(jcgregorio) Raise an error if loads fails?
760768
d = json.loads(content)
761769
self.token_response = d
762770
self.access_token = d['access_token']
@@ -785,7 +793,7 @@ def _do_refresh_request(self, http_request):
785793
self.invalid = True
786794
if self.store:
787795
self.store.locked_put(self)
788-
except StandardError:
796+
except (TypeError, ValueError):
789797
pass
790798
raise AccessTokenRefreshError(error_msg)
791799

@@ -822,7 +830,7 @@ def _do_revoke(self, http_request, token):
822830
d = json.loads(content)
823831
if 'error' in d:
824832
error_msg = d['error']
825-
except StandardError:
833+
except (TypeError, ValueError):
826834
pass
827835
raise TokenRevokeError(error_msg)
828836

@@ -880,10 +888,12 @@ def __init__(self, access_token, user_agent, revoke_uri=None):
880888

881889
@classmethod
882890
def from_json(cls, s):
891+
if six.PY3 and isinstance(s, bytes):
892+
s = s.decode('utf-8')
883893
data = json.loads(s)
884894
retval = AccessTokenCredentials(
885-
data['access_token'],
886-
data['user_agent'])
895+
data['access_token'],
896+
data['user_agent'])
887897
return retval
888898

889899
def _refresh(self, http_request):
@@ -903,7 +913,7 @@ def _revoke(self, http_request):
903913
_env_name = None
904914

905915

906-
def _get_environment(urllib2_urlopen=None):
916+
def _get_environment(urlopen=None):
907917
"""Detect the environment the code is being run on."""
908918

909919
global _env_name
@@ -917,16 +927,15 @@ def _get_environment(urllib2_urlopen=None):
917927
elif server_software.startswith('Development/'):
918928
_env_name = 'GAE_LOCAL'
919929
else:
920-
import urllib2
921930
try:
922-
if urllib2_urlopen is None:
923-
urllib2_urlopen = urllib2.urlopen
924-
response = urllib2_urlopen('http://metadata.google.internal')
931+
if urlopen is None:
932+
urlopen = urllib.request.urlopen
933+
response = urlopen('http://metadata.google.internal')
925934
if any('Metadata-Flavor: Google' in h for h in response.info().headers):
926935
_env_name = 'GCE_PRODUCTION'
927936
else:
928937
_env_name = 'UNKNOWN'
929-
except urllib2.URLError:
938+
except urllib.error.URLError:
930939
_env_name = 'UNKNOWN'
931940

932941
return _env_name
@@ -956,7 +965,7 @@ class GoogleCredentials(OAuth2Credentials):
956965
request = service.instances().list(project=PROJECT, zone=ZONE)
957966
response = request.execute()
958967
959-
print response
968+
print(response)
960969
</code>
961970
962971
A service that does not require authentication does not need credentials
@@ -970,7 +979,7 @@ class GoogleCredentials(OAuth2Credentials):
970979
request = service.apis().list()
971980
response = request.execute()
972981
973-
print response
982+
print(response)
974983
</code>
975984
"""
976985

@@ -1168,7 +1177,7 @@ def _get_application_default_credential_from_file(
11681177
application_default_credential_filename):
11691178
"""Build the Application Default Credentials from file."""
11701179

1171-
import service_account
1180+
from oauth2client import service_account
11721181

11731182
# read the credentials from the file
11741183
with open(application_default_credential_filename) as (
@@ -1274,7 +1283,7 @@ def __init__(self, assertion_type, user_agent=None,
12741283
def _generate_refresh_request_body(self):
12751284
assertion = self._generate_assertion()
12761285

1277-
body = urllib.urlencode({
1286+
body = urllib.parse.urlencode({
12781287
'assertion': assertion,
12791288
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
12801289
})
@@ -1363,6 +1372,8 @@ def __init__(self,
13631372

13641373
# Keep base64 encoded so it can be stored in JSON.
13651374
self.private_key = base64.b64encode(private_key)
1375+
if isinstance(self.private_key, six.text_type):
1376+
self.private_key = self.private_key.encode('utf-8')
13661377

13671378
self.private_key_password = private_key_password
13681379
self.service_account_name = service_account_name
@@ -1386,7 +1397,7 @@ def from_json(cls, s):
13861397

13871398
def _generate_assertion(self):
13881399
"""Generate the assertion that will be used in the request."""
1389-
now = long(time.time())
1400+
now = int(time.time())
13901401
payload = {
13911402
'aud': self.token_uri,
13921403
'scope': self.scope,
@@ -1435,16 +1446,17 @@ def verify_id_token(id_token, audience, http=None,
14351446
resp, content = http.request(cert_uri)
14361447

14371448
if resp.status == 200:
1438-
certs = json.loads(content)
1449+
certs = json.loads(content.decode('utf-8'))
14391450
return crypt.verify_signed_jwt_with_certs(id_token, certs, audience)
14401451
else:
14411452
raise VerifyJwtTokenError('Status code: %d' % resp.status)
14421453

14431454

14441455
def _urlsafe_b64decode(b64string):
14451456
# Guard against unicode strings, which base64 can't handle.
1446-
b64string = b64string.encode('ascii')
1447-
padded = b64string + '=' * (4 - len(b64string) % 4)
1457+
if isinstance(b64string, six.text_type):
1458+
b64string = b64string.encode('ascii')
1459+
padded = b64string + b'=' * (4 - len(b64string) % 4)
14481460
return base64.urlsafe_b64decode(padded)
14491461

14501462

@@ -1465,7 +1477,7 @@ def _extract_id_token(id_token):
14651477
raise VerifyJwtTokenError(
14661478
'Wrong number of segments in token: %s' % id_token)
14671479

1468-
return json.loads(_urlsafe_b64decode(segments[1]))
1480+
return json.loads(_urlsafe_b64decode(segments[1]).decode('utf-8'))
14691481

14701482

14711483
def _parse_exchange_token_response(content):
@@ -1483,11 +1495,11 @@ def _parse_exchange_token_response(content):
14831495
"""
14841496
resp = {}
14851497
try:
1486-
resp = json.loads(content)
1487-
except StandardError:
1498+
resp = json.loads(content.decode('utf-8'))
1499+
except Exception:
14881500
# different JSON libs raise different exceptions,
14891501
# so we just do a catch-all here
1490-
resp = dict(urlparse.parse_qsl(content))
1502+
resp = dict(urllib.parse.parse_qsl(content))
14911503

14921504
# some providers respond with 'expires', others with 'expires_in'
14931505
if resp and 'expires' in resp:
@@ -1810,7 +1822,7 @@ def step2_exchange(self, code=None, http=None, device_flow_info=None):
18101822
else:
18111823
post_data['grant_type'] = 'authorization_code'
18121824
post_data['redirect_uri'] = self.redirect_uri
1813-
body = urllib.urlencode(post_data)
1825+
body = urllib.parse.urlencode(post_data)
18141826
headers = {
18151827
'content-type': 'application/x-www-form-urlencoded',
18161828
}
@@ -1851,7 +1863,7 @@ def step2_exchange(self, code=None, http=None, device_flow_info=None):
18511863
logger.info('Failed to retrieve access token: %s', content)
18521864
if 'error' in d:
18531865
# you never know what those providers got to say
1854-
error_msg = unicode(d['error'])
1866+
error_msg = str(d['error'])
18551867
else:
18561868
error_msg = 'Invalid response: %s.' % str(resp.status)
18571869
raise FlowExchangeError(error_msg)

oauth2client/clientsecrets.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
__author__ = '[email protected] (Joe Gregorio)'
2222

2323
import json
24+
import six
2425

2526

2627
# Properties that make a client_secrets.json file valid.
@@ -70,9 +71,9 @@ class InvalidClientSecretsError(Error):
7071
def _validate_clientsecrets(obj):
7172
if obj is None or len(obj) != 1:
7273
raise InvalidClientSecretsError('Invalid file format.')
73-
client_type = obj.keys()[0]
74-
if client_type not in VALID_CLIENT.keys():
75-
raise InvalidClientSecretsError('Unknown client type: %s.' % client_type)
74+
client_type = tuple(obj)[0]
75+
if client_type not in VALID_CLIENT:
76+
raise InvalidClientSecretsError('Unknown client type: %s.' % (client_type,))
7677
client_info = obj[client_type]
7778
for prop_name in VALID_CLIENT[client_type]['required']:
7879
if prop_name not in client_info:
@@ -98,11 +99,8 @@ def loads(s):
9899

99100
def _loadfile(filename):
100101
try:
101-
fp = file(filename, 'r')
102-
try:
102+
with open(filename, 'r') as fp:
103103
obj = json.load(fp)
104-
finally:
105-
fp.close()
106104
except IOError:
107105
raise InvalidClientSecretsError('File not found: "%s"' % filename)
108106
return _validate_clientsecrets(obj)
@@ -150,4 +148,4 @@ def loadfile(filename, cache=None):
150148
obj = {client_type: client_info}
151149
cache.set(filename, obj, namespace=_SECRET_NAMESPACE)
152150

153-
return obj.iteritems().next()
151+
return next(six.iteritems(obj))

0 commit comments

Comments
 (0)