Author: jure
Date: Fri Mar 15 10:28:53 2013
New Revision: 1456865

URL: http://svn.apache.org/r1456865
Log:
#440, product environments as parametric singletons, patch 
t440_1456016_product_env_singleton.diff applied (from Olemis)


Modified:
    
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/cache.py
    
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/env.py
    
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/env.py

Modified: 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/cache.py
URL: 
http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/cache.py?rev=1456865&r1=1456864&r2=1456865&view=diff
==============================================================================
--- 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/cache.py
 (original)
+++ 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/cache.py
 Fri Mar 15 10:28:53 2013
@@ -3,6 +3,10 @@
 # Developed by Raymond Hettinger
 # (http://code.activestate.com/recipes/498245-lru-and-lfu-cache-decorators/)
 #
+# March 13, 2013 updated by Olemis Lang
+#    Added keymap arg to build custom keys out of actual args
+# March 14, 2013 updated by Olemis Lang
+#    Keep cache consistency on user function failure 
 
 import collections
 import functools
@@ -15,7 +19,7 @@ class Counter(dict):
     def __missing__(self, key):
         return 0
 
-def lru_cache(maxsize=100):
+def lru_cache(maxsize=100, keymap=None):
     '''Least-recently-used cache decorator.
 
     Arguments to the cached function must be hashable.
@@ -23,6 +27,8 @@ def lru_cache(maxsize=100):
     Clear the cache with f.clear().
     http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used
 
+    :param keymap:    build custom keys out of actual arguments.
+                      Its signature will be lambda (args, kwds, kwd_mark)
     '''
     maxqueue = maxsize * 10
     def decorating_function(user_function,
@@ -40,20 +46,32 @@ def lru_cache(maxsize=100):
         @functools.wraps(user_function)
         def wrapper(*args, **kwds):
             # cache key records both positional and keyword args
-            key = args
-            if kwds:
-                key += (kwd_mark,) + tuple(sorted(kwds.items()))
-
-            # record recent use of this key
-            queue_append(key)
-            refcount[key] += 1
+            if keymap is None:
+                key = args
+                if kwds:
+                    key += (kwd_mark,) + tuple(sorted(kwds.items()))
+            else:
+                key = keymap(args, kwds, kwd_mark)
 
             # get cache entry or compute if not found
             try:
                 result = cache[key]
                 wrapper.hits += 1
+
+                # record recent use of this key
+                queue_append(key)
+                refcount[key] += 1
             except KeyError:
-                result = user_function(*args, **kwds)
+                # Explicit exception handling for readability
+                try:
+                    result = user_function(*args, **kwds)
+                except:
+                    raise
+                else:
+                    # record recent use of this key
+                    queue_append(key)
+                    refcount[key] += 1
+
                 cache[key] = result
                 wrapper.misses += 1
 
@@ -91,7 +109,7 @@ def lru_cache(maxsize=100):
     return decorating_function
 
 
-def lfu_cache(maxsize=100):
+def lfu_cache(maxsize=100, keymap=None):
     '''Least-frequenty-used cache decorator.
 
     Arguments to the cached function must be hashable.
@@ -99,6 +117,8 @@ def lfu_cache(maxsize=100):
     Clear the cache with f.clear().
     http://en.wikipedia.org/wiki/Least_Frequently_Used
 
+    :param keymap:    build custom keys out of actual arguments.
+                      Its signature will be lambda (args, kwds, kwd_mark)
     '''
     def decorating_function(user_function):
         cache = {}                      # mapping of args to results
@@ -107,9 +127,12 @@ def lfu_cache(maxsize=100):
 
         @functools.wraps(user_function)
         def wrapper(*args, **kwds):
-            key = args
-            if kwds:
-                key += (kwd_mark,) + tuple(sorted(kwds.items()))
+            if keymap is None:
+                key = args
+                if kwds:
+                    key += (kwd_mark,) + tuple(sorted(kwds.items()))
+            else:
+                key = keymap(args, kwds, kwd_mark)
             use_count[key] += 1
 
             # get cache entry or compute if not found
@@ -140,3 +163,12 @@ def lfu_cache(maxsize=100):
         return wrapper
     return decorating_function
 
+#----------------------
+# Helper functions
+#----------------------
+
+def default_keymap(args, kwds, kwd_mark):
+    key = args
+    if kwds:
+        key += (kwd_mark,) + tuple(sorted(kwds.items()))
+    return key

Modified: 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/env.py
URL: 
http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/env.py?rev=1456865&r1=1456864&r2=1456865&view=diff
==============================================================================
--- 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/env.py
 (original)
+++ 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/env.py
 Fri Mar 15 10:28:53 2013
@@ -23,7 +23,8 @@ from urlparse import urlsplit
 from sqlite3 import OperationalError
 
 from trac.config import BoolOption, ConfigSection, Option
-from trac.core import Component, ComponentManager, implements, Interface, 
ExtensionPoint
+from trac.core import Component, ComponentManager, ComponentMeta, \
+        ExtensionPoint, implements, Interface
 from trac.db.api import TransactionContextManager, QueryContextManager, 
DatabaseManager
 from trac.util import get_pkginfo, lazy
 from trac.util.compat import sha1
@@ -32,6 +33,7 @@ from trac.versioncontrol import Reposito
 from trac.web.href import Href
 
 from multiproduct.api import MultiProductSystem, 
ISupportMultiProductEnvironment
+from multiproduct.cache import lru_cache, default_keymap
 from multiproduct.config import Configuration
 from multiproduct.dbcursor import ProductEnvContextManager, 
BloodhoundConnectionWrapper, BloodhoundIterableCursor
 from multiproduct.model import Product
@@ -319,7 +321,7 @@ class ProductEnvironment(Component, Comp
 
     Product environments contain among other things:
 
-    * a configuration file, 
+    * configuration key-value pairs stored in the database, 
     * product-aware clones of the wiki and ticket attachments files,
 
     Product environments do not have:
@@ -330,6 +332,32 @@ class ProductEnvironment(Component, Comp
 
     See https://issues.apache.org/bloodhound/wiki/Proposals/BEP-0003
     """
+    
+    class __metaclass__(ComponentMeta):
+
+        def product_env_keymap(args, kwds, kwd_mark):
+            # Remove meta-reference to self (i.e. product env class)
+            args = args[1:]
+            try:
+                product = kwds['product']
+            except KeyError:
+                # Product provided as positional argument
+                if isinstance(args[1], Product):
+                    args = (args[0], args[1].prefix) + args[2:]
+            else:
+                # Product supplied as keyword argument
+                if isinstance(product, Product):
+                    kwds['product'] = product.prefix
+            return default_keymap(args, kwds, kwd_mark)
+
+        @lru_cache(maxsize=100, keymap=product_env_keymap)
+        def __call__(self, *args, **kwargs):
+            """Return an existing instance of there is a hit 
+            in the global LRU cache, otherwise create a new instance.
+            """
+            return ComponentMeta.__call__(self, *args, **kwargs)
+
+        del product_env_keymap
 
     implements(trac.env.ISystemInfoProvider)
 
@@ -857,10 +885,4 @@ class ProductEnvironment(Component, Comp
 lookup_product_env = ProductEnvironment.lookup_env
 resolve_product_href = ProductEnvironment.resolve_href
 
-from multiproduct.cache import lru_cache
-
-@lru_cache(maxsize=100)
-def ProductEnvironmentFactory(global_env, product):
-    """Product environment factory
-    """
-    return ProductEnvironment(global_env, product)
+ProductEnvironmentFactory = ProductEnvironment

Modified: 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/env.py
URL: 
http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/env.py?rev=1456865&r1=1456864&r2=1456865&view=diff
==============================================================================
--- 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/env.py
 (original)
+++ 
incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/env.py
 Fri Mar 15 10:28:53 2013
@@ -490,6 +490,41 @@ class ProductEnvApiTestCase(Multiproduct
         self.assertEquals('value1', global_config['section'].get('key'))
         self.assertEquals('value2', product_config['section'].get('key'))
 
+    def test_parametric_singleton(self):
+        self.assertIs(self.product_env, 
+                      ProductEnvironment(self.env, self.default_product))
+
+        for prefix in self.PRODUCT_DATA:
+            if prefix != self.default_product:
+                self._load_product_from_data(self.env, prefix)
+
+        envgen1 = dict([prefix, ProductEnvironment(self.env, prefix)] 
+                   for prefix in self.PRODUCT_DATA)
+        envgen2 = dict([prefix, ProductEnvironment(self.env, prefix)] 
+                   for prefix in self.PRODUCT_DATA)
+
+        for prefix, env1 in envgen1.iteritems():
+            self.assertIs(env1, envgen2[prefix], 
+                          "Identity check (by prefix) '%s'" % (prefix,))
+
+        for prefix, env1 in envgen1.iteritems():
+            self.assertIs(env1, envgen2[prefix], 
+                          "Identity check (by prefix) '%s'" % (prefix,))
+
+        def load_product(prefix):
+            products = Product.select(self.env, where={'prefix' : prefix})
+            if not products:
+                raise LookupError('Missing product %s' % (prefix,))
+            else:
+                return products[0]
+
+        envgen3 = dict([prefix, ProductEnvironment(self.env, 
+                                                   load_product(prefix))] 
+                   for prefix in self.PRODUCT_DATA)
+
+        for prefix, env1 in envgen1.iteritems():
+            self.assertIs(env1, envgen3[prefix], 
+                          "Identity check (by product model) '%s'" % (prefix,))
 
 def test_suite():
     return unittest.TestSuite([


Reply via email to