Skip to content

Commit 56f60bb

Browse files
committed
Merge pull request #667 from dhermes/lazy-loading-attempt-4
Adding lazy loading support for dataset ID.
2 parents 26dba83 + b9d96ed commit 56f60bb

File tree

7 files changed

+159
-26
lines changed

7 files changed

+159
-26
lines changed

gcloud/datastore/_implicit_environ.py

+70-21
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,6 @@
3333
_GCD_DATASET_ENV_VAR_NAME = 'DATASTORE_DATASET'
3434

3535

36-
class _DefaultsContainer(object):
37-
"""Container for defaults.
38-
39-
:type connection: :class:`gcloud.datastore.connection.Connection`
40-
:param connection: Persistent implied connection from environment.
41-
42-
:type dataset_id: string
43-
:param dataset_id: Persistent implied dataset ID from environment.
44-
"""
45-
46-
def __init__(self, connection=None, dataset_id=None, implicit=False):
47-
self.implicit = implicit
48-
self.connection = connection
49-
self.dataset_id = dataset_id
50-
51-
5236
def app_engine_id():
5337
"""Gets the App Engine application ID if it can be inferred.
5438
@@ -150,6 +134,15 @@ def set_default_dataset_id(dataset_id=None):
150134
raise EnvironmentError('No dataset ID could be inferred.')
151135

152136

137+
def get_default_dataset_id():
138+
"""Get default dataset ID.
139+
140+
:rtype: string or ``NoneType``
141+
:returns: The default dataset ID if one has been set.
142+
"""
143+
return _DEFAULTS.dataset_id
144+
145+
153146
def get_default_connection():
154147
"""Get default connection.
155148
@@ -159,13 +152,69 @@ def get_default_connection():
159152
return _DEFAULTS.connection
160153

161154

162-
def get_default_dataset_id():
163-
"""Get default dataset ID.
155+
class _LazyProperty(object):
156+
"""Descriptor for lazy loaded property.
164157
165-
:rtype: string or ``NoneType``
166-
:returns: The default dataset ID if one has been set.
158+
This follows the reify pattern: lazy evaluation and then replacement
159+
after evaluation.
160+
161+
:type name: string
162+
:param name: The name of the attribute / property being evaluated.
163+
164+
:type deferred_callable: callable that takes no arguments
165+
:param deferred_callable: The function / method used to evaluate the
166+
property.
167167
"""
168-
return _DEFAULTS.dataset_id
168+
169+
def __init__(self, name, deferred_callable):
170+
self._name = name
171+
self._deferred_callable = deferred_callable
172+
173+
def __get__(self, obj, objtype):
174+
if obj is None or objtype is not _DefaultsContainer:
175+
return self
176+
177+
setattr(obj, self._name, self._deferred_callable())
178+
return getattr(obj, self._name)
179+
180+
181+
def _lazy_property_deco(deferred_callable):
182+
"""Decorator a method to create a :class:`_LazyProperty`.
183+
184+
:type deferred_callable: callable that takes no arguments
185+
:param deferred_callable: The function / method used to evaluate the
186+
property.
187+
188+
:rtype: :class:`_LazyProperty`.
189+
:returns: A lazy property which defers the deferred_callable.
190+
"""
191+
if isinstance(deferred_callable, staticmethod):
192+
# H/T: http://stackoverflow.com/a/9527450/1068170
193+
# For Python2.7+ deferred_callable.__func__ would suffice.
194+
deferred_callable = deferred_callable.__get__(True)
195+
return _LazyProperty(deferred_callable.__name__, deferred_callable)
196+
197+
198+
class _DefaultsContainer(object):
199+
"""Container for defaults.
200+
201+
:type connection: :class:`gcloud.datastore.connection.Connection`
202+
:param connection: Persistent implied connection from environment.
203+
204+
:type dataset_id: string
205+
:param dataset_id: Persistent implied dataset ID from environment.
206+
"""
207+
208+
@_lazy_property_deco
209+
@staticmethod
210+
def dataset_id():
211+
"""Return the implicit default dataset ID."""
212+
return _determine_default_dataset_id()
213+
214+
def __init__(self, connection=None, dataset_id=None, implicit=False):
215+
self.connection = connection
216+
if dataset_id is not None or not implicit:
217+
self.dataset_id = dataset_id
169218

170219

171220
_DEFAULTS = _DefaultsContainer(implicit=True)

gcloud/datastore/_testing.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ def _monkey_defaults(*args, **kwargs):
2424
return _Monkey(_implicit_environ, _DEFAULTS=mock_defaults)
2525

2626

27-
def _setup_defaults(test_case):
27+
def _setup_defaults(test_case, *args, **kwargs):
2828
test_case._replaced_defaults = _implicit_environ._DEFAULTS
29-
_implicit_environ._DEFAULTS = _DefaultsContainer()
29+
_implicit_environ._DEFAULTS = _DefaultsContainer(*args, **kwargs)
3030

3131

3232
def _tear_down_defaults(test_case):

gcloud/datastore/test__implicit_environ.py

+76
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,82 @@ def test_set_implicit_three_env_appengine_and_compute(self):
321321
self.assertEqual(connection.timeout, None)
322322

323323

324+
class Test__lazy_property_deco(unittest2.TestCase):
325+
326+
def _callFUT(self, deferred_callable):
327+
from gcloud.datastore._implicit_environ import _lazy_property_deco
328+
return _lazy_property_deco(deferred_callable)
329+
330+
def test_on_function(self):
331+
def test_func():
332+
pass # pragma: NO COVER never gets called
333+
334+
lazy_prop = self._callFUT(test_func)
335+
self.assertTrue(lazy_prop._deferred_callable is test_func)
336+
self.assertEqual(lazy_prop._name, 'test_func')
337+
338+
def test_on_staticmethod(self):
339+
def test_func():
340+
pass # pragma: NO COVER never gets called
341+
342+
lazy_prop = self._callFUT(staticmethod(test_func))
343+
self.assertTrue(lazy_prop._deferred_callable is test_func)
344+
self.assertEqual(lazy_prop._name, 'test_func')
345+
346+
347+
class Test_lazy_loaded_dataset_id(unittest2.TestCase):
348+
349+
def setUp(self):
350+
from gcloud.datastore._testing import _setup_defaults
351+
_setup_defaults(self, implicit=True)
352+
353+
def tearDown(self):
354+
from gcloud.datastore._testing import _tear_down_defaults
355+
_tear_down_defaults(self)
356+
357+
def test_prop_default(self):
358+
from gcloud.datastore import _implicit_environ
359+
from gcloud.datastore._implicit_environ import _DefaultsContainer
360+
from gcloud.datastore._implicit_environ import _LazyProperty
361+
362+
self.assertTrue(isinstance(_DefaultsContainer.dataset_id,
363+
_LazyProperty))
364+
self.assertEqual(_implicit_environ._DEFAULTS.dataset_id, None)
365+
366+
def test_prop_on_wrong_class(self):
367+
from gcloud.datastore._implicit_environ import _LazyProperty
368+
369+
# Don't actually need a callable for ``method`` since
370+
# __get__ will just return ``self`` in this test.
371+
data_prop = _LazyProperty('dataset_id', None)
372+
373+
class FakeEnv(object):
374+
dataset_id = data_prop
375+
376+
self.assertTrue(FakeEnv.dataset_id is data_prop)
377+
self.assertTrue(FakeEnv().dataset_id is data_prop)
378+
379+
def test_prop_descriptor(self):
380+
from gcloud._testing import _Monkey
381+
from gcloud.datastore import _implicit_environ
382+
383+
self.assertFalse(
384+
'dataset_id' in _implicit_environ._DEFAULTS.__dict__)
385+
386+
DEFAULT = object()
387+
388+
def mock_default():
389+
return DEFAULT
390+
391+
with _Monkey(_implicit_environ,
392+
_determine_default_dataset_id=mock_default):
393+
lazy_loaded = _implicit_environ._DEFAULTS.dataset_id
394+
395+
self.assertEqual(lazy_loaded, DEFAULT)
396+
self.assertTrue(
397+
'dataset_id' in _implicit_environ._DEFAULTS.__dict__)
398+
399+
324400
class _AppIdentity(object):
325401

326402
def __init__(self, app_id):

pylintrc_default

+5
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,18 @@ ignore =
7373
# identical implementation but different docstrings.
7474
# - star-args: standard Python idioms for varargs:
7575
# ancestor = Query().filter(*order_props)
76+
# - method-hidden: Decorating a method in a class (e.g. in _DefaultsContainer)
77+
# @_lazy_property_deco
78+
# def dataset_id():
79+
# ...
7680
disable =
7781
maybe-no-member,
7882
no-member,
7983
protected-access,
8084
redefined-builtin,
8185
similarities,
8286
star-args,
87+
method-hidden,
8388

8489

8590
[REPORTS]

regression/clear_datastore.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
from six.moves import input
1818

1919
from gcloud import datastore
20+
from gcloud.datastore import _implicit_environ
2021

2122

22-
datastore._DATASET_ENV_VAR_NAME = 'GCLOUD_TESTS_DATASET_ID'
23+
_implicit_environ._DATASET_ENV_VAR_NAME = 'GCLOUD_TESTS_DATASET_ID'
2324
datastore.set_defaults()
2425

2526

regression/datastore.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
import unittest2
1818

1919
from gcloud import datastore
20+
from gcloud.datastore import _implicit_environ
2021
# This assumes the command is being run via tox hence the
2122
# repository root is the current directory.
2223
from regression import populate_datastore
2324

2425

25-
datastore._DATASET_ENV_VAR_NAME = 'GCLOUD_TESTS_DATASET_ID'
26+
_implicit_environ._DATASET_ENV_VAR_NAME = 'GCLOUD_TESTS_DATASET_ID'
2627
datastore.set_defaults()
2728

2829

regression/populate_datastore.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
from six.moves import zip
1818

1919
from gcloud import datastore
20+
from gcloud.datastore import _implicit_environ
2021

2122

22-
datastore._DATASET_ENV_VAR_NAME = 'GCLOUD_TESTS_DATASET_ID'
23+
_implicit_environ._DATASET_ENV_VAR_NAME = 'GCLOUD_TESTS_DATASET_ID'
2324
datastore.set_defaults()
2425

2526

0 commit comments

Comments
 (0)