Add memcache proxy preparing to factor out caching

Signed-off-by: martin f. krafft <madduck@madduck.net>
diff --git a/reclass/storage/memcache_proxy.py b/reclass/storage/memcache_proxy.py
new file mode 100644
index 0000000..7d9ab5e
--- /dev/null
+++ b/reclass/storage/memcache_proxy.py
@@ -0,0 +1,65 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–14 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+
+from reclass.storage import NodeStorageBase
+
+STORAGE_NAME = 'memcache_proxy'
+
+class MemcacheProxy(NodeStorageBase):
+
+    def __init__(self, real_storage, cache_classes=True, cache_nodes=True,
+                 cache_nodelist=True):
+        name = '{0}({1})'.format(STORAGE_NAME, real_storage.name)
+        super(MemcacheProxy, self).__init__(name)
+        self._real_storage = real_storage
+        self._cache_classes = cache_classes
+        if cache_classes:
+            self._classes_cache = {}
+        self._cache_nodes = cache_nodes
+        if cache_nodes:
+            self._nodes_cache = {}
+        self._cache_nodelist = cache_nodelist
+        if cache_nodelist:
+            self._nodelist_cache = None
+
+    name = property(lambda self: self._real_storage.name)
+
+    @staticmethod
+    def _cache_proxy(name, cache, getter):
+        try:
+            ret = cache[name]
+
+        except KeyError, e:
+            ret = getter(name)
+            cache[name] = ret
+
+        return ret
+
+    def get_node(self, name):
+        if not self._cache_nodes:
+            return self._real_storage.get_node(name)
+
+        return MemcacheProxy._cache_proxy(name, self._nodes_cache,
+                                          self._real_storage.get_node)
+
+    def get_class(self, name):
+        if not self._cache_classes:
+            return self._real_storage.get_class(name)
+
+        return MemcacheProxy._cache_proxy(name, self._classes_cache,
+                                          self._real_storage.get_class)
+
+    def enumerate_nodes(self):
+        if not self._cache_nodelist:
+            return self._real_storage.enumerate_nodes()
+
+        elif self._nodelist_cache is None:
+            self._nodelist_cache = self._real_storage.enumerate_nodes()
+
+        return self._nodelist_cache
diff --git a/reclass/storage/tests/__init__.py b/reclass/storage/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/reclass/storage/tests/__init__.py
diff --git a/reclass/storage/tests/test_memcache_proxy.py b/reclass/storage/tests/test_memcache_proxy.py
new file mode 100644
index 0000000..066c27e
--- /dev/null
+++ b/reclass/storage/tests/test_memcache_proxy.py
@@ -0,0 +1,85 @@
+#
+# -*- coding: utf-8 -*-
+#
+# This file is part of reclass (http://github.com/madduck/reclass)
+#
+# Copyright © 2007–14 martin f. krafft <madduck@madduck.net>
+# Released under the terms of the Artistic Licence 2.0
+#
+from reclass.storage.memcache_proxy import MemcacheProxy
+from reclass.storage import NodeStorageBase
+
+import unittest
+try:
+    import unittest.mock as mock
+except ImportError:
+    import mock
+
+class TestMemcacheProxy(unittest.TestCase):
+
+    def setUp(self):
+        self._storage = mock.MagicMock(spec_set=NodeStorageBase)
+
+    def test_no_nodes_caching(self):
+        p = MemcacheProxy(self._storage, cache_nodes=False)
+        NAME = 'foo'; NAME2 = 'bar'; RET = 'baz'
+        self._storage.get_node.return_value = RET
+        self.assertEqual(p.get_node(NAME), RET)
+        self.assertEqual(p.get_node(NAME), RET)
+        self.assertEqual(p.get_node(NAME2), RET)
+        self.assertEqual(p.get_node(NAME2), RET)
+        expected = [mock.call(NAME), mock.call(NAME),
+                    mock.call(NAME2), mock.call(NAME2)]
+        self.assertListEqual(self._storage.get_node.call_args_list, expected)
+
+    def test_nodes_caching(self):
+        p = MemcacheProxy(self._storage, cache_nodes=True)
+        NAME = 'foo'; NAME2 = 'bar'; RET = 'baz'
+        self._storage.get_node.return_value = RET
+        self.assertEqual(p.get_node(NAME), RET)
+        self.assertEqual(p.get_node(NAME), RET)
+        self.assertEqual(p.get_node(NAME2), RET)
+        self.assertEqual(p.get_node(NAME2), RET)
+        expected = [mock.call(NAME), mock.call(NAME2)] # called once each
+        self.assertListEqual(self._storage.get_node.call_args_list, expected)
+
+    def test_no_classes_caching(self):
+        p = MemcacheProxy(self._storage, cache_classes=False)
+        NAME = 'foo'; NAME2 = 'bar'; RET = 'baz'
+        self._storage.get_class.return_value = RET
+        self.assertEqual(p.get_class(NAME), RET)
+        self.assertEqual(p.get_class(NAME), RET)
+        self.assertEqual(p.get_class(NAME2), RET)
+        self.assertEqual(p.get_class(NAME2), RET)
+        expected = [mock.call(NAME), mock.call(NAME),
+                    mock.call(NAME2), mock.call(NAME2)]
+        self.assertListEqual(self._storage.get_class.call_args_list, expected)
+
+    def test_classes_caching(self):
+        p = MemcacheProxy(self._storage, cache_classes=True)
+        NAME = 'foo'; NAME2 = 'bar'; RET = 'baz'
+        self._storage.get_class.return_value = RET
+        self.assertEqual(p.get_class(NAME), RET)
+        self.assertEqual(p.get_class(NAME), RET)
+        self.assertEqual(p.get_class(NAME2), RET)
+        self.assertEqual(p.get_class(NAME2), RET)
+        expected = [mock.call(NAME), mock.call(NAME2)] # called once each
+        self.assertListEqual(self._storage.get_class.call_args_list, expected)
+
+    def test_nodelist_no_caching(self):
+        p = MemcacheProxy(self._storage, cache_nodelist=False)
+        p.enumerate_nodes()
+        p.enumerate_nodes()
+        expected = [mock.call(), mock.call()]
+        self.assertListEqual(self._storage.enumerate_nodes.call_args_list, expected)
+
+    def test_nodelist_caching(self):
+        p = MemcacheProxy(self._storage, cache_nodelist=True)
+        p.enumerate_nodes()
+        p.enumerate_nodes()
+        expected = [mock.call()] # once only
+        self.assertListEqual(self._storage.enumerate_nodes.call_args_list, expected)
+
+
+if __name__ == '__main__':
+    unittest.main()