Repository: libcloud Updated Branches: refs/heads/trunk 94c6b9143 -> 9a7556f46
Implemented LazyObject class, which provides a .lazy class method. The lazy class method returns a Proxy object that subclasses the target object class. Upon accessing the proxy object in any way, the object is initialized. Modified Google Compute Engine License objects, GCELicense, to be such a lazy object. This addresses https://issues.apache.org/jira/browse/LIBCLOUD-786. Tests/Verification: tox -e lint python setup.py test Added test/common/test_base.py which has LazyObjectTest Signed-off-by: Eric Johnson <erjoh...@google.com> Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/81a8dbc3 Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/81a8dbc3 Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/81a8dbc3 Branch: refs/heads/trunk Commit: 81a8dbc324f870444e980aee523fea48fc37cbcc Parents: 94c6b91 Author: Scott Crunkleton <crunkle...@google.com> Authored: Tue Dec 22 00:21:06 2015 -0800 Committer: Eric Johnson <erjoh...@google.com> Committed: Tue Jan 5 22:29:27 2016 +0000 ---------------------------------------------------------------------- libcloud/common/base.py | 35 +++++++++++++++++++ libcloud/compute/drivers/gce.py | 63 ++++++++++++++++------------------ libcloud/test/common/test_base.py | 44 ++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 33 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/81a8dbc3/libcloud/common/base.py ---------------------------------------------------------------------- diff --git a/libcloud/common/base.py b/libcloud/common/base.py index 0825eca..959215a 100644 --- a/libcloud/common/base.py +++ b/libcloud/common/base.py @@ -76,6 +76,41 @@ __all__ = [ RETRY_FAILED_HTTP_REQUESTS = False +class LazyObject(object): + """An object that doesn't get initialized until accessed.""" + + @classmethod + def _proxy(cls, *lazy_init_args, **lazy_init_kwargs): + class Proxy(cls, object): + _lazy_obj = None + + def __init__(self): + # Must override the lazy_cls __init__ + pass + + def __getattribute__(self, attr): + lazy_obj = object.__getattribute__(self, '_get_lazy_obj')() + return getattr(lazy_obj, attr) + + def __setattr__(self, attr, value): + lazy_obj = object.__getattribute__(self, '_get_lazy_obj')() + setattr(lazy_obj, attr, value) + + def _get_lazy_obj(self): + lazy_obj = object.__getattribute__(self, '_lazy_obj') + if lazy_obj is None: + lazy_obj = cls(*lazy_init_args, **lazy_init_kwargs) + object.__setattr__(self, '_lazy_obj', lazy_obj) + return lazy_obj + + return Proxy() + + @classmethod + def lazy(cls, *lazy_init_args, **lazy_init_kwargs): + """Create a lazily instantiated instance of the subclass, cls.""" + return cls._proxy(*lazy_init_args, **lazy_init_kwargs) + + class HTTPResponse(httplib.HTTPResponse): # On python 2.6 some calls can hang because HEAD isn't quite properly # supported. http://git-wip-us.apache.org/repos/asf/libcloud/blob/81a8dbc3/libcloud/compute/drivers/gce.py ---------------------------------------------------------------------- diff --git a/libcloud/compute/drivers/gce.py b/libcloud/compute/drivers/gce.py index 84ac64d..4a5181e 100644 --- a/libcloud/compute/drivers/gce.py +++ b/libcloud/compute/drivers/gce.py @@ -21,6 +21,7 @@ import datetime import time import sys +from libcloud.common.base import LazyObject from libcloud.common.google import GoogleResponse from libcloud.common.google import GoogleBaseConnection from libcloud.common.google import GoogleBaseError @@ -231,15 +232,37 @@ class GCEList(object): return self -class GCELicense(UuidMixin): +class GCELicense(UuidMixin, LazyObject): """A GCE License used to track software usage in GCE nodes.""" - def __init__(self, id, name, driver, charges_use_fee, extra=None): - self.id = str(id) + def __init__(self, name, project, driver): + UuidMixin.__init__(self) + self.id = name self.name = name + self.project = project self.driver = driver - self.charges_use_fee = charges_use_fee - self.extra = extra or {} - UuidMixin.__init__(self) + self.charges_use_fee = None # init in _request + self.extra = None # init in _request + + self._request() + + def _request(self): + # TODO(crunkle...@google.com): create new connection? or make + # connection thread-safe? Saving, modifying, and restoring + # driver.connection.request_path is really hacky and thread-unsafe. + saved_request_path = self.driver.connection.request_path + new_request_path = saved_request_path.replace(self.driver.project, + self.project) + self.driver.connection.request_path = new_request_path + + request = '/global/licenses/%s' % self.name + response = self.driver.connection.request(request, method='GET').object + self.driver.connection.request_path = saved_request_path + + self.extra = { + 'selfLink': response.get('selfLink'), + 'kind': response.get('kind') + } + self.charges_use_fee = response['chargesUseFee'] def destroy(self): raise ProviderError("Can not destroy a License resource.") @@ -3913,15 +3936,7 @@ class GCENodeDriver(NodeDriver): :return: A DiskType object for the name :rtype: :class:`GCEDiskType` """ - saved_request_path = self.connection.request_path - new_request_path = saved_request_path.replace(self.project, project) - self.connection.request_path = new_request_path - - request = '/global/licenses/%s' % (name) - response = self.connection.request(request, method='GET').object - self.connection.request_path = saved_request_path - - return self._to_license(response) + return GCELicense.lazy(name, project, self) def ex_get_disktype(self, name, zone=None): """ @@ -5691,24 +5706,6 @@ class GCENodeDriver(NodeDriver): maintenance_windows=zone.get('maintenanceWindows'), deprecated=deprecated, driver=self, extra=extra) - def _to_license(self, license): - """ - Return a License object from the JSON-response dictionary. - - :param license: The dictionary describing the license. - :type license: ``dict`` - - :return: License object - :rtype: :class:`GCELicense` - """ - extra = {} - extra['selfLink'] = license.get('selfLink') - extra['kind'] = license.get('kind') - - return GCELicense(id=license['name'], name=license['name'], - charges_use_fee=license['chargesUseFee'], - driver=self, extra=extra) - def _set_project_metadata(self, metadata=None, force=False, current_keys=""): """ http://git-wip-us.apache.org/repos/asf/libcloud/blob/81a8dbc3/libcloud/test/common/test_base.py ---------------------------------------------------------------------- diff --git a/libcloud/test/common/test_base.py b/libcloud/test/common/test_base.py new file mode 100644 index 0000000..9c7070d --- /dev/null +++ b/libcloud/test/common/test_base.py @@ -0,0 +1,44 @@ +import unittest +import sys + +import mock + +from libcloud.common.base import LazyObject +from libcloud.test import LibcloudTestCase + + +class LazyObjectTest(LibcloudTestCase): + + class A(LazyObject): + def __init__(self, x, y=None): + self.x = x + self.y = y + + def test_lazy_init(self): + # Test normal init + a = self.A(1, y=2) + self.assertTrue(isinstance(a, self.A)) + + # Test lazy init + with mock.patch.object(self.A, + '__init__', return_value=None) as mock_init: + a = self.A.lazy(3, y=4) + self.assertTrue(isinstance(a, self.A)) # Proxy is a subclass of A + mock_init.assert_not_called() + + # Since we have a mock init, an A object doesn't actually get + # created. But, we can still call __dict__ on the proxy, which will + # init the lazy object. + self.assertEqual(a.__dict__, {}) + mock_init.assert_called_once_with(3, y=4) + + def test_setattr(self): + a = self.A.lazy('foo', y='bar') + a.z = 'baz' + wrapped_lazy_obj = object.__getattribute__(a, '_lazy_obj') + self.assertEqual(a.z, 'baz') + self.assertEqual(wrapped_lazy_obj.z, 'baz') + + +if __name__ == '__main__': + sys.exit(unittest.main())