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())

Reply via email to