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