Skip to content

Adding lazy loading behavior for default project (storage). #735

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 17, 2015
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions gcloud/_localstack.py → gcloud/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,46 @@ def top(self):
"""
if len(self._stack) > 0:
return self._stack[-1]


class _LazyProperty(object):
"""Descriptor for lazy loaded property.

This follows the reify pattern: lazy evaluation and then replacement
after evaluation.

:type name: string
:param name: The name of the attribute / property being evaluated.

:type deferred_callable: callable that takes no arguments
:param deferred_callable: The function / method used to evaluate the
property.
"""

def __init__(self, name, deferred_callable):
self._name = name
self._deferred_callable = deferred_callable

def __get__(self, obj, objtype):
if obj is None:
return self

setattr(obj, self._name, self._deferred_callable())
return getattr(obj, self._name)


def _lazy_property_deco(deferred_callable):
"""Decorator a method to create a :class:`_LazyProperty`.

:type deferred_callable: callable that takes no arguments
:param deferred_callable: The function / method used to evaluate the
property.

:rtype: :class:`_LazyProperty`.
:returns: A lazy property which defers the deferred_callable.
"""
if isinstance(deferred_callable, staticmethod):
# H/T: http://stackoverflow.com/a/9527450/1068170
# For Python2.7+ deferred_callable.__func__ would suffice.
deferred_callable = deferred_callable.__get__(True)
return _LazyProperty(deferred_callable.__name__, deferred_callable)
44 changes: 1 addition & 43 deletions gcloud/datastore/_implicit_environ.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
except ImportError:
app_identity = None

from gcloud._helpers import _lazy_property_deco
from gcloud import credentials
from gcloud.datastore.connection import Connection

Expand Down Expand Up @@ -201,49 +202,6 @@ def get_default_connection():
return _DEFAULTS.connection


class _LazyProperty(object):
"""Descriptor for lazy loaded property.

This follows the reify pattern: lazy evaluation and then replacement
after evaluation.

:type name: string
:param name: The name of the attribute / property being evaluated.

:type deferred_callable: callable that takes no arguments
:param deferred_callable: The function / method used to evaluate the
property.
"""

def __init__(self, name, deferred_callable):
self._name = name
self._deferred_callable = deferred_callable

def __get__(self, obj, objtype):
if obj is None or objtype is not _DefaultsContainer:
return self

setattr(obj, self._name, self._deferred_callable())
return getattr(obj, self._name)


def _lazy_property_deco(deferred_callable):
"""Decorator a method to create a :class:`_LazyProperty`.

:type deferred_callable: callable that takes no arguments
:param deferred_callable: The function / method used to evaluate the
property.

:rtype: :class:`_LazyProperty`.
:returns: A lazy property which defers the deferred_callable.
"""
if isinstance(deferred_callable, staticmethod):
# H/T: http://stackoverflow.com/a/9527450/1068170
# For Python2.7+ deferred_callable.__func__ would suffice.
deferred_callable = deferred_callable.__get__(True)
return _LazyProperty(deferred_callable.__name__, deferred_callable)


class _DefaultsContainer(object):
"""Container for defaults.

Expand Down
2 changes: 1 addition & 1 deletion gcloud/datastore/batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

"""Create / interact with a batch of updates / deletes."""

from gcloud._localstack import _LocalStack
from gcloud._helpers import _LocalStack
from gcloud.datastore import _implicit_environ
from gcloud.datastore import helpers
from gcloud.datastore.key import _dataset_ids_equal
Expand Down
36 changes: 0 additions & 36 deletions gcloud/datastore/test__implicit_environ.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,29 +315,6 @@ def mock_determine(dataset_id):
self.assertEqual(_called_dataset_id, [None])


class Test__lazy_property_deco(unittest2.TestCase):

def _callFUT(self, deferred_callable):
from gcloud.datastore._implicit_environ import _lazy_property_deco
return _lazy_property_deco(deferred_callable)

def test_on_function(self):
def test_func():
pass # pragma: NO COVER never gets called

lazy_prop = self._callFUT(test_func)
self.assertTrue(lazy_prop._deferred_callable is test_func)
self.assertEqual(lazy_prop._name, 'test_func')

def test_on_staticmethod(self):
def test_func():
pass # pragma: NO COVER never gets called

lazy_prop = self._callFUT(staticmethod(test_func))
self.assertTrue(lazy_prop._deferred_callable is test_func)
self.assertEqual(lazy_prop._name, 'test_func')


class Test_lazy_loading(unittest2.TestCase):

def setUp(self):
Expand All @@ -348,19 +325,6 @@ def tearDown(self):
from gcloud.datastore._testing import _tear_down_defaults
_tear_down_defaults(self)

def test_prop_on_wrong_class(self):
from gcloud.datastore._implicit_environ import _LazyProperty

# Don't actually need a callable for ``method`` since
# __get__ will just return ``self`` in this test.
data_prop = _LazyProperty('dataset_id', None)

class FakeEnv(object):
dataset_id = data_prop

self.assertTrue(FakeEnv.dataset_id is data_prop)
self.assertTrue(FakeEnv().dataset_id is data_prop)

def test_descriptor_for_dataset_id(self):
from gcloud._testing import _Monkey
from gcloud.datastore import _implicit_environ
Expand Down
21 changes: 1 addition & 20 deletions gcloud/storage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from gcloud.storage._implicit_environ import get_default_bucket
from gcloud.storage._implicit_environ import get_default_connection
from gcloud.storage._implicit_environ import get_default_project
from gcloud.storage._implicit_environ import set_default_project
from gcloud.storage.api import create_bucket
from gcloud.storage.api import get_all_buckets
from gcloud.storage.api import get_bucket
Expand All @@ -59,7 +60,6 @@
'https://www.googleapis.com/auth/devstorage.read_write')

_BUCKET_ENV_VAR_NAME = 'GCLOUD_BUCKET_NAME'
_PROJECT_ENV_VAR_NAME = 'GCLOUD_PROJECT'


def set_default_bucket(bucket=None):
Expand Down Expand Up @@ -88,25 +88,6 @@ def set_default_bucket(bucket=None):
_implicit_environ._DEFAULTS.bucket = bucket


def set_default_project(project=None):
"""Set default bucket name either explicitly or implicitly as fall-back.

In implicit case, currently only supports enviroment variable but will
support App Engine, Compute Engine and other environments in the future.

Local environment variable used is:
- GCLOUD_PROJECT

:type project: string
:param project: Optional. The project name to use as default.
"""
if project is None:
project = os.getenv(_PROJECT_ENV_VAR_NAME)

if project is not None:
_implicit_environ._DEFAULTS.project = project


def set_default_connection(connection=None):
"""Set default connection either explicitly or implicitly as fall-back.

Expand Down
63 changes: 60 additions & 3 deletions gcloud/storage/_implicit_environ.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,55 @@
"""


import os

from gcloud._helpers import _lazy_property_deco


_PROJECT_ENV_VAR_NAME = 'GCLOUD_PROJECT'


def _get_production_project():
"""Gets the production project if it can be inferred."""
return os.getenv(_PROJECT_ENV_VAR_NAME)


def _determine_default_project(project=None):
"""Determine default project ID explicitly or implicitly as fall-back.

In implicit case, currently only supports enviroment variable but will
support App Engine, Compute Engine and other environments in the future.

Local environment variable used is:
- GCLOUD_PROJECT

:type project: string
:param project: Optional. The project name to use as default.

:rtype: string or ``NoneType``
:returns: Default project if it can be determined.
"""
if project is None:
project = _get_production_project()

return project


def set_default_project(project=None):
"""Set default project either explicitly or implicitly as fall-back.

:type project: string
:param project: Optional. The project name to use as default.

:raises: :class:`EnvironmentError` if no project was found.
"""
project = _determine_default_project(project=project)
if project is not None:
_DEFAULTS.project = project
else:
raise EnvironmentError('No project could be inferred.')


class _DefaultsContainer(object):
"""Container for defaults.

Expand All @@ -32,8 +81,16 @@ class _DefaultsContainer(object):
:param connection: Persistent implied connection from environment.
"""

def __init__(self, project=None, bucket=None, connection=None):
self.project = project
@_lazy_property_deco
@staticmethod
def project():
"""Return the implicit default project."""
return _determine_default_project()

def __init__(self, project=None, bucket=None, connection=None,
implicit=False):
if project is not None or not implicit:
self.project = project
self.bucket = bucket
self.connection = connection

Expand Down Expand Up @@ -65,4 +122,4 @@ def get_default_connection():
return _DEFAULTS.connection


_DEFAULTS = _DefaultsContainer()
_DEFAULTS = _DefaultsContainer(implicit=True)
2 changes: 1 addition & 1 deletion gcloud/storage/batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

import six

from gcloud._localstack import _LocalStack
from gcloud._helpers import _LocalStack
from gcloud.storage import _implicit_environ
from gcloud.storage.connection import Connection

Expand Down
67 changes: 0 additions & 67 deletions gcloud/storage/test___init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,73 +135,6 @@ def test_set_explicit_None_w_env_var_set(self):
self.assertEqual(default_bucket.connection, CONNECTION)


class Test_set_default_project(unittest2.TestCase):

def setUp(self):
from gcloud.storage._testing import _setup_defaults
_setup_defaults(self)

def tearDown(self):
from gcloud.storage._testing import _tear_down_defaults
_tear_down_defaults(self)

def _callFUT(self, project=None):
from gcloud.storage import set_default_project
return set_default_project(project=project)

def _monkey(self, implicit_project):
import os
from gcloud.storage import _PROJECT_ENV_VAR_NAME
from gcloud._testing import _Monkey
environ = {_PROJECT_ENV_VAR_NAME: implicit_project}
return _Monkey(os, getenv=environ.get)

def test_no_env_var_set(self):
from gcloud.storage import _implicit_environ
with self._monkey(None):
self._callFUT()
self.assertEqual(_implicit_environ.get_default_project(), None)

def test_set_from_env_var(self):
from gcloud.storage import _implicit_environ
IMPLICIT_PROJECT = 'IMPLICIT'
with self._monkey(IMPLICIT_PROJECT):
self._callFUT()
self.assertEqual(_implicit_environ.get_default_project(),
IMPLICIT_PROJECT)

def test_set_explicit_w_env_var_set(self):
from gcloud.storage import _implicit_environ
EXPLICIT_PROJECT = 'EXPLICIT'
with self._monkey(None):
self._callFUT(EXPLICIT_PROJECT)
self.assertEqual(_implicit_environ.get_default_project(),
EXPLICIT_PROJECT)

def test_set_explicit_no_env_var_set(self):
from gcloud.storage import _implicit_environ
IMPLICIT_PROJECT = 'IMPLICIT'
EXPLICIT_PROJECT = 'EXPLICIT'
with self._monkey(IMPLICIT_PROJECT):
self._callFUT(EXPLICIT_PROJECT)
self.assertEqual(_implicit_environ.get_default_project(),
EXPLICIT_PROJECT)

def test_set_explicit_None_wo_env_var_set(self):
from gcloud.storage import _implicit_environ
with self._monkey(None):
self._callFUT(None)
self.assertEqual(_implicit_environ.get_default_project(), None)

def test_set_explicit_None_w_env_var_set(self):
from gcloud.storage import _implicit_environ
IMPLICIT_PROJECT = 'IMPLICIT'
with self._monkey(IMPLICIT_PROJECT):
self._callFUT(None)
self.assertEqual(_implicit_environ.get_default_project(),
IMPLICIT_PROJECT)


class Test_set_default_connection(unittest2.TestCase):

def setUp(self):
Expand Down
Loading