Skip to content

Commit b22630b

Browse files
authored
feat: add support for using the emulator programatically (#87)
* feat: add support for using the emulator programatically * always set credentials when SPANNER_EMULATOR_HOST is set * address PR comments Co-authored-by: larkee <[email protected]>
1 parent f33c866 commit b22630b

File tree

3 files changed

+99
-25
lines changed

3 files changed

+99
-25
lines changed

google/cloud/spanner_v1/client.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import warnings
2929

3030
from google.api_core.gapic_v1 import client_info
31+
from google.auth.credentials import AnonymousCredentials
3132
import google.api_core.client_options
3233

3334
from google.cloud.spanner_admin_instance_v1.gapic.transports import (
@@ -173,19 +174,27 @@ def __init__(
173174
client_options=None,
174175
query_options=None,
175176
):
177+
self._emulator_host = _get_spanner_emulator_host()
178+
179+
if client_options and type(client_options) == dict:
180+
self._client_options = google.api_core.client_options.from_dict(
181+
client_options
182+
)
183+
else:
184+
self._client_options = client_options
185+
186+
if self._emulator_host:
187+
credentials = AnonymousCredentials()
188+
elif isinstance(credentials, AnonymousCredentials):
189+
self._emulator_host = self._client_options.api_endpoint
190+
176191
# NOTE: This API has no use for the _http argument, but sending it
177192
# will have no impact since the _http() @property only lazily
178193
# creates a working HTTP object.
179194
super(Client, self).__init__(
180195
project=project, credentials=credentials, _http=None
181196
)
182197
self._client_info = client_info
183-
if client_options and type(client_options) == dict:
184-
self._client_options = google.api_core.client_options.from_dict(
185-
client_options
186-
)
187-
else:
188-
self._client_options = client_options
189198

190199
env_query_options = ExecuteSqlRequest.QueryOptions(
191200
optimizer_version=_get_spanner_optimizer_version()
@@ -198,9 +207,8 @@ def __init__(
198207
warnings.warn(_USER_AGENT_DEPRECATED, DeprecationWarning, stacklevel=2)
199208
self.user_agent = user_agent
200209

201-
if _get_spanner_emulator_host() is not None and (
202-
"http://" in _get_spanner_emulator_host()
203-
or "https://" in _get_spanner_emulator_host()
210+
if self._emulator_host is not None and (
211+
"http://" in self._emulator_host or "https://" in self._emulator_host
204212
):
205213
warnings.warn(_EMULATOR_HOST_HTTP_SCHEME)
206214

@@ -237,9 +245,9 @@ def project_name(self):
237245
def instance_admin_api(self):
238246
"""Helper for session-related API calls."""
239247
if self._instance_admin_api is None:
240-
if _get_spanner_emulator_host() is not None:
248+
if self._emulator_host is not None:
241249
transport = instance_admin_grpc_transport.InstanceAdminGrpcTransport(
242-
channel=grpc.insecure_channel(_get_spanner_emulator_host())
250+
channel=grpc.insecure_channel(target=self._emulator_host)
243251
)
244252
self._instance_admin_api = InstanceAdminClient(
245253
client_info=self._client_info,
@@ -258,9 +266,9 @@ def instance_admin_api(self):
258266
def database_admin_api(self):
259267
"""Helper for session-related API calls."""
260268
if self._database_admin_api is None:
261-
if _get_spanner_emulator_host() is not None:
269+
if self._emulator_host is not None:
262270
transport = database_admin_grpc_transport.DatabaseAdminGrpcTransport(
263-
channel=grpc.insecure_channel(_get_spanner_emulator_host())
271+
channel=grpc.insecure_channel(target=self._emulator_host)
264272
)
265273
self._database_admin_api = DatabaseAdminClient(
266274
client_info=self._client_info,
@@ -363,7 +371,7 @@ def instance(
363371
configuration_name,
364372
node_count,
365373
display_name,
366-
_get_spanner_emulator_host(),
374+
self._emulator_host,
367375
)
368376

369377
def list_instances(self, filter_="", page_size=None, page_token=None):

google/cloud/spanner_v1/database.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,9 +223,7 @@ def spanner_api(self):
223223
channel=grpc.insecure_channel(self._instance.emulator_host)
224224
)
225225
self._spanner_api = SpannerClient(
226-
client_info=client_info,
227-
client_options=client_options,
228-
transport=transport,
226+
client_info=client_info, transport=transport
229227
)
230228
return self._spanner_api
231229
credentials = self._instance._client.credentials

tests/unit/test_client.py

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,14 @@ def _constructor_test_helper(
110110
@mock.patch("warnings.warn")
111111
def test_constructor_emulator_host_warning(self, mock_warn, mock_em):
112112
from google.cloud.spanner_v1 import client as MUT
113+
from google.auth.credentials import AnonymousCredentials
113114

114-
expected_scopes = (MUT.SPANNER_ADMIN_SCOPE,)
115+
expected_scopes = None
115116
creds = _make_credentials()
116117
mock_em.return_value = "http://emulator.host.com"
117-
self._constructor_test_helper(expected_scopes, creds)
118+
with mock.patch("google.cloud.spanner_v1.client.AnonymousCredentials") as patch:
119+
expected_creds = patch.return_value = AnonymousCredentials()
120+
self._constructor_test_helper(expected_scopes, creds, expected_creds)
118121
mock_warn.assert_called_once_with(MUT._EMULATOR_HOST_HTTP_SCHEME)
119122

120123
def test_constructor_default_scopes(self):
@@ -219,6 +222,8 @@ def test_constructor_custom_query_options_env_config(self, mock_ver):
219222
def test_instance_admin_api(self, mock_em):
220223
from google.cloud.spanner_v1.client import SPANNER_ADMIN_SCOPE
221224

225+
mock_em.return_value = None
226+
222227
credentials = _make_credentials()
223228
client_info = mock.Mock()
224229
client_options = mock.Mock()
@@ -230,7 +235,6 @@ def test_instance_admin_api(self, mock_em):
230235
)
231236
expected_scopes = (SPANNER_ADMIN_SCOPE,)
232237

233-
mock_em.return_value = None
234238
inst_module = "google.cloud.spanner_v1.client.InstanceAdminClient"
235239
with mock.patch(inst_module) as instance_admin_client:
236240
api = client.instance_admin_api
@@ -250,7 +254,8 @@ def test_instance_admin_api(self, mock_em):
250254
credentials.with_scopes.assert_called_once_with(expected_scopes)
251255

252256
@mock.patch("google.cloud.spanner_v1.client._get_spanner_emulator_host")
253-
def test_instance_admin_api_emulator(self, mock_em):
257+
def test_instance_admin_api_emulator_env(self, mock_em):
258+
mock_em.return_value = "emulator.host"
254259
credentials = _make_credentials()
255260
client_info = mock.Mock()
256261
client_options = mock.Mock()
@@ -261,7 +266,38 @@ def test_instance_admin_api_emulator(self, mock_em):
261266
client_options=client_options,
262267
)
263268

264-
mock_em.return_value = "true"
269+
inst_module = "google.cloud.spanner_v1.client.InstanceAdminClient"
270+
with mock.patch(inst_module) as instance_admin_client:
271+
api = client.instance_admin_api
272+
273+
self.assertIs(api, instance_admin_client.return_value)
274+
275+
# API instance is cached
276+
again = client.instance_admin_api
277+
self.assertIs(again, api)
278+
279+
self.assertEqual(len(instance_admin_client.call_args_list), 1)
280+
called_args, called_kw = instance_admin_client.call_args
281+
self.assertEqual(called_args, ())
282+
self.assertEqual(called_kw["client_info"], client_info)
283+
self.assertEqual(called_kw["client_options"], client_options)
284+
self.assertIn("transport", called_kw)
285+
self.assertNotIn("credentials", called_kw)
286+
287+
def test_instance_admin_api_emulator_code(self):
288+
from google.auth.credentials import AnonymousCredentials
289+
from google.api_core.client_options import ClientOptions
290+
291+
credentials = AnonymousCredentials()
292+
client_info = mock.Mock()
293+
client_options = ClientOptions(api_endpoint="emulator.host")
294+
client = self._make_one(
295+
project=self.PROJECT,
296+
credentials=credentials,
297+
client_info=client_info,
298+
client_options=client_options,
299+
)
300+
265301
inst_module = "google.cloud.spanner_v1.client.InstanceAdminClient"
266302
with mock.patch(inst_module) as instance_admin_client:
267303
api = client.instance_admin_api
@@ -284,6 +320,7 @@ def test_instance_admin_api_emulator(self, mock_em):
284320
def test_database_admin_api(self, mock_em):
285321
from google.cloud.spanner_v1.client import SPANNER_ADMIN_SCOPE
286322

323+
mock_em.return_value = None
287324
credentials = _make_credentials()
288325
client_info = mock.Mock()
289326
client_options = mock.Mock()
@@ -295,7 +332,6 @@ def test_database_admin_api(self, mock_em):
295332
)
296333
expected_scopes = (SPANNER_ADMIN_SCOPE,)
297334

298-
mock_em.return_value = None
299335
db_module = "google.cloud.spanner_v1.client.DatabaseAdminClient"
300336
with mock.patch(db_module) as database_admin_client:
301337
api = client.database_admin_api
@@ -315,7 +351,8 @@ def test_database_admin_api(self, mock_em):
315351
credentials.with_scopes.assert_called_once_with(expected_scopes)
316352

317353
@mock.patch("google.cloud.spanner_v1.client._get_spanner_emulator_host")
318-
def test_database_admin_api_emulator(self, mock_em):
354+
def test_database_admin_api_emulator_env(self, mock_em):
355+
mock_em.return_value = "host:port"
319356
credentials = _make_credentials()
320357
client_info = mock.Mock()
321358
client_options = mock.Mock()
@@ -326,7 +363,38 @@ def test_database_admin_api_emulator(self, mock_em):
326363
client_options=client_options,
327364
)
328365

329-
mock_em.return_value = "host:port"
366+
db_module = "google.cloud.spanner_v1.client.DatabaseAdminClient"
367+
with mock.patch(db_module) as database_admin_client:
368+
api = client.database_admin_api
369+
370+
self.assertIs(api, database_admin_client.return_value)
371+
372+
# API instance is cached
373+
again = client.database_admin_api
374+
self.assertIs(again, api)
375+
376+
self.assertEqual(len(database_admin_client.call_args_list), 1)
377+
called_args, called_kw = database_admin_client.call_args
378+
self.assertEqual(called_args, ())
379+
self.assertEqual(called_kw["client_info"], client_info)
380+
self.assertEqual(called_kw["client_options"], client_options)
381+
self.assertIn("transport", called_kw)
382+
self.assertNotIn("credentials", called_kw)
383+
384+
def test_database_admin_api_emulator_code(self):
385+
from google.auth.credentials import AnonymousCredentials
386+
from google.api_core.client_options import ClientOptions
387+
388+
credentials = AnonymousCredentials()
389+
client_info = mock.Mock()
390+
client_options = ClientOptions(api_endpoint="emulator.host")
391+
client = self._make_one(
392+
project=self.PROJECT,
393+
credentials=credentials,
394+
client_info=client_info,
395+
client_options=client_options,
396+
)
397+
330398
db_module = "google.cloud.spanner_v1.client.DatabaseAdminClient"
331399
with mock.patch(db_module) as database_admin_client:
332400
api = client.database_admin_api

0 commit comments

Comments
 (0)