Skip to content

Commit a55010f

Browse files
authored
Adding optional switch to capture project ID in from_service_account_json(). (#3436)
Fixes #1883.
1 parent 92a20c6 commit a55010f

File tree

5 files changed

+82
-10
lines changed

5 files changed

+82
-10
lines changed

bigtable/google/cloud/bigtable/client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ class Client(_ClientFactoryMixin, _ClientProjectMixin):
207207
_instance_stub_internal = None
208208
_operations_stub_internal = None
209209
_table_stub_internal = None
210+
_SET_PROJECT = True # Used by from_service_account_json()
210211

211212
def __init__(self, project=None, credentials=None,
212213
read_only=False, admin=False, user_agent=DEFAULT_USER_AGENT):

core/google/cloud/client.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
"""Base classes for client used to interact with Google Cloud APIs."""
1616

17+
import io
18+
import json
1719
from pickle import PicklingError
1820

1921
import google.auth.credentials
@@ -40,6 +42,8 @@ class _ClientFactoryMixin(object):
4042
This class is virtual.
4143
"""
4244

45+
_SET_PROJECT = False
46+
4347
@classmethod
4448
def from_service_account_json(cls, json_credentials_path, *args, **kwargs):
4549
"""Factory to retrieve JSON credentials while creating client.
@@ -58,15 +62,21 @@ def from_service_account_json(cls, json_credentials_path, *args, **kwargs):
5862
:type kwargs: dict
5963
:param kwargs: Remaining keyword arguments to pass to constructor.
6064
61-
:rtype: :class:`google.cloud.pubsub.client.Client`
65+
:rtype: :class:`_ClientFactoryMixin`
6266
:returns: The client created with the retrieved JSON credentials.
6367
:raises: :class:`TypeError` if there is a conflict with the kwargs
6468
and the credentials created by the factory.
6569
"""
6670
if 'credentials' in kwargs:
6771
raise TypeError('credentials must not be in keyword arguments')
68-
credentials = service_account.Credentials.from_service_account_file(
69-
json_credentials_path)
72+
with io.open(json_credentials_path, 'r', encoding='utf-8') as json_fi:
73+
credentials_info = json.load(json_fi)
74+
credentials = service_account.Credentials.from_service_account_info(
75+
credentials_info)
76+
if cls._SET_PROJECT:
77+
if 'project' not in kwargs:
78+
kwargs['project'] = credentials_info.get('project_id')
79+
7080
kwargs['credentials'] = credentials
7181
return cls(*args, **kwargs)
7282

@@ -207,6 +217,8 @@ class ClientWithProject(Client, _ClientProjectMixin):
207217
set in the environment.
208218
"""
209219

220+
_SET_PROJECT = True # Used by from_service_account_json()
221+
210222
def __init__(self, project=None, credentials=None, _http=None):
211223
_ClientProjectMixin.__init__(self, project=project)
212224
Client.__init__(self, credentials=credentials, _http=_http)

core/nox.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ def unit_tests(session, python_version):
3333
session.install('-e', '.')
3434

3535
# Run py.test against the unit tests.
36-
session.run('py.test', '--quiet',
36+
session.run(
37+
'py.test', '--quiet',
3738
'--cov=google.cloud', '--cov=tests.unit', '--cov-append',
3839
'--cov-config=.coveragerc', '--cov-report=', '--cov-fail-under=97',
3940
'tests/unit',

core/tests/unit/test_client.py

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import io
16+
import json
1517
import unittest
1618

1719
import mock
@@ -90,21 +92,32 @@ def test_ctor_bad_credentials(self):
9092
self._make_one(credentials=CREDENTIALS)
9193

9294
def test_from_service_account_json(self):
93-
KLASS = self._get_target_class()
95+
from google.cloud import _helpers
96+
97+
klass = self._get_target_class()
9498

99+
# Mock both the file opening and the credentials constructor.
100+
info = {'dummy': 'value', 'valid': 'json'}
101+
json_fi = io.StringIO(_helpers._bytes_to_unicode(json.dumps(info)))
102+
file_open_patch = mock.patch(
103+
'io.open', return_value=json_fi)
95104
constructor_patch = mock.patch(
96105
'google.oauth2.service_account.Credentials.'
97-
'from_service_account_file',
106+
'from_service_account_info',
98107
return_value=_make_credentials())
99108

100-
with constructor_patch as constructor:
101-
client_obj = KLASS.from_service_account_json(
102-
mock.sentinel.filename)
109+
with file_open_patch as file_open:
110+
with constructor_patch as constructor:
111+
client_obj = klass.from_service_account_json(
112+
mock.sentinel.filename)
103113

104114
self.assertIs(
105115
client_obj._credentials, constructor.return_value)
106116
self.assertIsNone(client_obj._http_internal)
107-
constructor.assert_called_once_with(mock.sentinel.filename)
117+
# Check that mocks were called as expected.
118+
file_open.assert_called_once_with(
119+
mock.sentinel.filename, 'r', encoding='utf-8')
120+
constructor.assert_called_once_with(info)
108121

109122
def test_from_service_account_json_bad_args(self):
110123
KLASS = self._get_target_class()
@@ -221,3 +234,47 @@ def test_ctor_explicit_bytes(self):
221234
def test_ctor_explicit_unicode(self):
222235
PROJECT = u'PROJECT'
223236
self._explicit_ctor_helper(PROJECT)
237+
238+
def _from_service_account_json_helper(self, project=None):
239+
from google.cloud import _helpers
240+
241+
klass = self._get_target_class()
242+
243+
info = {'dummy': 'value', 'valid': 'json'}
244+
if project is None:
245+
expected_project = 'eye-d-of-project'
246+
else:
247+
expected_project = project
248+
249+
info['project_id'] = expected_project
250+
# Mock both the file opening and the credentials constructor.
251+
json_fi = io.StringIO(_helpers._bytes_to_unicode(json.dumps(info)))
252+
file_open_patch = mock.patch(
253+
'io.open', return_value=json_fi)
254+
constructor_patch = mock.patch(
255+
'google.oauth2.service_account.Credentials.'
256+
'from_service_account_info',
257+
return_value=_make_credentials())
258+
259+
with file_open_patch as file_open:
260+
with constructor_patch as constructor:
261+
kwargs = {}
262+
if project is not None:
263+
kwargs['project'] = project
264+
client_obj = klass.from_service_account_json(
265+
mock.sentinel.filename, **kwargs)
266+
267+
self.assertIs(
268+
client_obj._credentials, constructor.return_value)
269+
self.assertIsNone(client_obj._http_internal)
270+
self.assertEqual(client_obj.project, expected_project)
271+
# Check that mocks were called as expected.
272+
file_open.assert_called_once_with(
273+
mock.sentinel.filename, 'r', encoding='utf-8')
274+
constructor.assert_called_once_with(info)
275+
276+
def test_from_service_account_json(self):
277+
self._from_service_account_json_helper()
278+
279+
def test_from_service_account_json_project_set(self):
280+
self._from_service_account_json_helper(project='prah-jekt')

spanner/google/cloud/spanner/client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ class Client(_ClientFactoryMixin, _ClientProjectMixin):
102102
"""
103103
_instance_admin_api = None
104104
_database_admin_api = None
105+
_SET_PROJECT = True # Used by from_service_account_json()
105106

106107
def __init__(self, project=None, credentials=None,
107108
user_agent=DEFAULT_USER_AGENT):

0 commit comments

Comments
 (0)