Merge pull request #218 from odoo-dev/master-ormcache_new_api-rco

[MERGE] ormcache: improve API to enable the decoration of new-api methods

The new signature of `ormcache()` is more intuitive: simply list the names of the method arguments that compose the cache key. The decorators `ormcache_context()` and `ormcache_multi()` have been adapted accordingly.
This commit is contained in:
Raphael Collet
2015-05-19 11:40:46 +02:00
19 changed files with 230 additions and 165 deletions
@@ -39,30 +39,29 @@ class decimal_precision(orm.Model):
('name_uniq', 'unique (name)', """Only one value can be defined for each given usage!"""),
]
@tools.ormcache(skiparg=3)
@tools.ormcache('application')
def precision_get(self, cr, uid, application):
cr.execute('select digits from decimal_precision where name=%s', (application,))
res = cr.fetchone()
return res[0] if res else 2
def clear_cache(self, cr):
"""clear cache and update models. Notify other workers to restart their registry."""
self.precision_get.clear_cache(self)
RegistryManager.signal_registry_change(cr.dbname)
""" Deprecated, use `clear_caches` instead. """
self.clear_caches()
def create(self, cr, uid, data, context=None):
res = super(decimal_precision, self).create(cr, uid, data, context=context)
self.clear_cache(cr)
self.clear_caches()
return res
def unlink(self, cr, uid, ids, context=None):
res = super(decimal_precision, self).unlink(cr, uid, ids, context=context)
self.clear_cache(cr)
self.clear_caches()
return res
def write(self, cr, uid, ids, data, *args, **argv):
res = super(decimal_precision, self).write(cr, uid, ids, data, *args, **argv)
self.clear_cache(cr)
self.clear_caches()
return res
+1 -1
View File
@@ -152,7 +152,7 @@ class view(osv.osv):
return arch
@tools.ormcache_context(accepted_keys=('website_id',))
@tools.ormcache_context('uid', 'xml_id', keys=('website_id',))
def get_view_id(self, cr, uid, xml_id, context=None):
if context and 'website_id' in context and not isinstance(xml_id, (int, long)):
domain = [('key', '=', xml_id), '|', ('website_id', '=', context['website_id']), ('website_id', '=', False)]
+5 -5
View File
@@ -333,8 +333,8 @@ class website(osv.osv):
except:
return False
@openerp.tools.ormcache(skiparg=3)
def _get_languages(self, cr, uid, id):
@openerp.tools.ormcache('id')
def _get_languages(self, cr, uid, id, context=None):
website = self.browse(cr, uid, id)
return [(lg.code, lg.name) for lg in website.language_ids]
@@ -375,9 +375,9 @@ class website(osv.osv):
lang['hreflang'] = lang['short']
return langs
@openerp.tools.ormcache(skiparg=3)
def _get_current_website_id(self, cr, uid, domain_name):
ids = self.search(cr, uid, [('name', '=', domain_name)], limit=1)
@openerp.tools.ormcache('domain_name')
def _get_current_website_id(self, cr, uid, domain_name, context=None):
ids = self.search(cr, uid, [('name', '=', domain_name)], limit=1, context=context)
if ids:
return ids[0]
else:
+6 -5
View File
@@ -135,8 +135,7 @@ class website(orm.Model):
string='Price list available for this Ecommerce/Website'),
}
@tools.ormcache()
# No context, else no cache
@tools.ormcache('uid', 'country_code', 'show_visible', 'website_pl', 'current_pl', 'all_pl')
def _get_pl(self, cr, uid, country_code, show_visible, website_pl, current_pl, all_pl):
""" Return the list of pricelists that can be used on website for the current user.
@@ -362,10 +361,12 @@ class website_pricelist(osv.Model):
'website_pricelist_id', 'res_country_group_id', string='Country Groups'),
}
# _get_pl is cached to avoid to recompute at each request the list of pricelists availables.
# So, we need to invalidate the cache when we change the config of website price list to force to recompute.
def clear_cache(self):
self.pool['website']._get_pl.clear_cache(self.pool['website'])
# website._get_pl() is cached to avoid to recompute at each request the
# list of available pricelists. So, we need to invalidate the cache when
# we change the config of website price list to force to recompute.
website = self.pool['website']
website._get_pl.clear_cache(website)
def create(self, cr, uid, data, context=None):
res = super(website_pricelist, self).create(cr, uid, data, context=context)
+2 -1
View File
@@ -62,7 +62,8 @@ class view(models.Model):
xml_id = super(view, self).get_view_id(xml_id)
return xml_id
@tools.ormcache_context(**dict(accepted_keys=('lang', 'inherit_branding', 'editable', 'translatable', 'website_id', 'version_id')))
@tools.ormcache_context('uid', 'view_id',
keys=('lang', 'inherit_branding', 'editable', 'translatable', 'website_id', 'version_id'))
def _read_template(self, cr, uid, view_id, context=None):
arch = self.read_combined(cr, uid, view_id, fields=['arch'], context=context)['arch']
arch_tree = etree.fromstring(arch)
@@ -81,7 +81,7 @@ class ir_config_parameter(osv.osv):
return default
return result
@ormcache(skiparg=2) # cache on (uid, key)
@ormcache('uid', 'key')
def _get_param(self, cr, uid, key):
params = self.search_read(cr, uid, [('key', '=', key)], fields=['value'], limit=1)
if not params:
+2 -2
View File
@@ -733,7 +733,7 @@ class ir_model_access(osv.osv):
# But as the method raises an exception in that case, the key 'lang' might
# not be really necessary as a cache key, unless the `ormcache_context`
# decorator catches the exception (it does not at the moment.)
@tools.ormcache_context(accepted_keys=('lang',))
@tools.ormcache_context('uid', 'model', 'mode', 'raise_exception', keys=('lang',))
def check(self, cr, uid, model, mode='read', raise_exception=True, context=None):
if uid==1:
# User root have all accesses
@@ -913,7 +913,7 @@ class ir_model_data(osv.osv):
cr.execute('CREATE INDEX ir_model_data_module_name_index ON ir_model_data (module, name)')
# NEW V8 API
@tools.ormcache(skiparg=3)
@tools.ormcache('xmlid')
def xmlid_lookup(self, cr, uid, xmlid):
"""Low level xmlid lookup
Return (id, res_model, res_id) or raise ValueError if not found
+6 -5
View File
@@ -103,7 +103,7 @@ class ir_rule(osv.osv):
(_check_model_name, 'Rules can not be applied on the Record Rules model.', ['model_id']),
]
@tools.ormcache()
@tools.ormcache('uid', 'model_name', 'mode')
def _compute_domain(self, cr, uid, model_name, mode="read"):
if mode not in self._MODES:
raise ValueError('Invalid mode: %r' % (mode,))
@@ -144,7 +144,8 @@ class ir_rule(osv.osv):
return []
def clear_cache(self, cr, uid):
self._compute_domain.clear_cache(self)
""" Deprecated, use `clear_caches` instead. """
self.clear_caches()
def domain_get(self, cr, uid, model_name, mode='read', context=None):
dom = self._compute_domain(cr, uid, model_name, mode)
@@ -159,15 +160,15 @@ class ir_rule(osv.osv):
def unlink(self, cr, uid, ids, context=None):
res = super(ir_rule, self).unlink(cr, uid, ids, context=context)
self.clear_cache(cr, uid)
self.clear_caches()
return res
def create(self, cr, uid, vals, context=None):
res = super(ir_rule, self).create(cr, uid, vals, context=context)
self.clear_cache(cr, uid)
self.clear_caches()
return res
def write(self, cr, uid, ids, vals, context=None):
res = super(ir_rule, self).write(cr, uid, ids, vals, context=context)
self.clear_cache(cr,uid)
self.clear_caches()
return res
+2 -2
View File
@@ -269,7 +269,7 @@ class ir_translation(osv.osv):
return
return super(ir_translation, self)._check_selection_field_value(cr, uid, field, value, context=context)
@tools.ormcache_multi(skiparg=3, multi=6)
@tools.ormcache_multi('name', 'tt', 'lang', multi='ids')
def _get_ids(self, cr, uid, name, tt, lang, ids):
translations = dict.fromkeys(ids, False)
if ids:
@@ -333,7 +333,7 @@ class ir_translation(osv.osv):
return (query, params)
@tools.ormcache(skiparg=3)
@tools.ormcache('name', 'types', 'lang', 'source', 'res_id')
def __get_source(self, cr, uid, name, types, lang, source, res_id):
# res_id is a tuple or None, otherwise ormcache cannot cache it!
query, params = self._get_source_query(cr, uid, name, types, lang, source, res_id)
+49 -64
View File
@@ -38,23 +38,51 @@ class ir_ui_menu(osv.osv):
_name = 'ir.ui.menu'
def __init__(self, *args, **kwargs):
cls = type(self)
# by design, self._menu_cache is specific to the database
cls._menu_cache_lock = threading.RLock()
cls._menu_cache = {}
super(ir_ui_menu, self).__init__(*args, **kwargs)
self.pool.get('ir.model.access').register_cache_clearing_method(self._name, 'clear_cache')
self.pool['ir.model.access'].register_cache_clearing_method(self._name, 'clear_caches')
def clear_cache(self):
with self._menu_cache_lock:
# radical but this doesn't frequently happen
if self._menu_cache:
# Normally this is done by openerp.tools.ormcache
# but since we do not use it, set it by ourself.
self.pool._any_cache_cleared = True
self._menu_cache.clear()
self.load_menus_root._orig.clear_cache(self)
self.load_menus._orig.clear_cache(self)
""" Deprecated, use `clear_caches` instead. """
self.clear_caches()
@api.model
@tools.ormcache('frozenset(self.env.user.groups_id.ids)')
def _visible_menu_ids(self):
""" Return the ids of the menu items visible to the user. """
# retrieve all menus, and determine which ones are visible
context = {'ir.ui.menu.full_list': True}
menus = self.with_context(context).search([])
# first discard all menus with groups the user does not have
groups = self.env.user.groups_id
menus = menus.filtered(
lambda menu: not menu.groups_id or menu.groups_id & groups)
# take apart menus that have an action
action_menus = menus.filtered('action')
folder_menus = menus - action_menus
visible = self.browse()
# process action menus, check whether their action is allowed
access = self.env['ir.model.access']
model_fname = {
'ir.actions.act_window': 'res_model',
'ir.actions.report.xml': 'model',
'ir.actions.wizard': 'model',
'ir.actions.server': 'model_id',
}
for menu in action_menus:
fname = model_fname.get(menu.action._name)
if not fname or not menu.action[fname] or \
access.check(menu.action[fname], 'read', False):
# make menu visible, and its folder ancestors, too
visible += menu
menu = menu.parent_id
while menu and menu in folder_menus and menu not in visible:
visible += menu
menu = menu.parent_id
return set(visible.ids)
@api.multi
@api.returns('self')
@@ -63,51 +91,8 @@ class ir_ui_menu(osv.osv):
the menu hierarchy of the current user.
Uses a cache for speeding up the computation.
"""
with self._menu_cache_lock:
groups = self.env.user.groups_id
# visibility is entirely based on the user's groups;
# self._menu_cache[key] gives the ids of all visible menus
key = frozenset(groups._ids)
if key in self._menu_cache:
visible = self.browse(self._menu_cache[key])
else:
# retrieve all menus, and determine which ones are visible
context = {'ir.ui.menu.full_list': True}
menus = self.with_context(context).search([])
# first discard all menus with groups the user does not have
menus = menus.filtered(
lambda menu: not menu.groups_id or menu.groups_id & groups)
# take apart menus that have an action
action_menus = menus.filtered('action')
folder_menus = menus - action_menus
visible = self.browse()
# process action menus, check whether their action is allowed
access = self.env['ir.model.access']
model_fname = {
'ir.actions.act_window': 'res_model',
'ir.actions.report.xml': 'model',
'ir.actions.wizard': 'model',
'ir.actions.server': 'model_id',
}
for menu in action_menus:
fname = model_fname.get(menu.action._name)
if not fname or not menu.action[fname] or \
access.check(menu.action[fname], 'read', False):
# make menu visible, and its folder ancestors, too
visible += menu
menu = menu.parent_id
while menu and menu in folder_menus and menu not in visible:
visible += menu
menu = menu.parent_id
self._menu_cache[key] = visible._ids
return self.filtered(lambda menu: menu in visible)
visible_ids = self._visible_menu_ids()
return self.filtered(lambda menu: menu.id in visible_ids)
def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
if context is None:
@@ -161,11 +146,11 @@ class ir_ui_menu(osv.osv):
return parent_path + elmt.name
def create(self, cr, uid, values, context=None):
self.clear_cache()
self.clear_caches()
return super(ir_ui_menu, self).create(cr, uid, values, context=context)
def write(self, cr, uid, ids, values, context=None):
self.clear_cache()
self.clear_caches()
return super(ir_ui_menu, self).write(cr, uid, ids, values, context=context)
def unlink(self, cr, uid, ids, context=None):
@@ -182,7 +167,7 @@ class ir_ui_menu(osv.osv):
self.write(cr, uid, direct_children_ids, {'parent_id': False})
result = super(ir_ui_menu, self).unlink(cr, uid, ids, context=context)
self.clear_cache()
self.clear_caches()
return result
def copy(self, cr, uid, id, default=None, context=None):
@@ -356,7 +341,7 @@ class ir_ui_menu(osv.osv):
return self.search(cr, uid, menu_domain, context=context)
@api.cr_uid_context
@tools.ormcache_context(accepted_keys=('lang',))
@tools.ormcache_context('uid', keys=('lang',))
def load_menus_root(self, cr, uid, context=None):
fields = ['name', 'sequence', 'parent_id', 'action', 'icon']
menu_root_ids = self.get_user_roots(cr, uid, context=context)
@@ -371,7 +356,7 @@ class ir_ui_menu(osv.osv):
@api.cr_uid_context
@tools.ormcache_context(accepted_keys=('lang',))
@tools.ormcache_context('uid', keys=('lang',))
def load_menus(self, cr, uid, context=None):
""" Loads all menu items (all applications and their sub-menus).
+9 -8
View File
@@ -320,7 +320,7 @@ class view(osv.osv):
if not values.get('name'):
values['name'] = "%s %s" % (values.get('model'), values['type'])
self.clear_cache()
self.clear_caches()
return super(view, self).create(
cr, uid,
self._compute_defaults(cr, uid, values, context=context),
@@ -343,7 +343,7 @@ class view(osv.osv):
if custom_view_ids:
self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)
self.clear_cache()
self.clear_caches()
ret = super(view, self).write(
cr, uid, ids,
self._compute_defaults(cr, uid, vals, context=context),
@@ -925,11 +925,11 @@ class view(osv.osv):
#------------------------------------------------------
# QWeb template views
#------------------------------------------------------
_read_template_cache = dict(accepted_keys=('lang','inherit_branding', 'editable', 'translatable'))
if config['dev_mode']:
_read_template_cache['size'] = 0
@tools.conditional(not tools.config['dev_mode'], tools.ormcache_context(**_read_template_cache))
# apply ormcache_context decorator unless in dev mode...
@tools.conditional(not config['dev_mode'],
tools.ormcache_context('uid', 'view_id',
keys=('lang', 'inherit_branding', 'editable', 'translatable')))
def _read_template(self, cr, uid, view_id, context=None):
arch = self.read_combined(cr, uid, view_id, fields=['arch'], context=context)['arch']
arch_tree = etree.fromstring(arch)
@@ -956,7 +956,8 @@ class view(osv.osv):
return self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, xml_id, raise_if_not_found=True)
def clear_cache(self):
self._read_template.clear_cache(self)
""" Deprecated, use `clear_caches` instead. """
self.clear_caches()
def _contains_branded(self, node):
return node.tag == 't'\
@@ -1070,7 +1071,7 @@ class view(osv.osv):
self._translate_qweb(cr, uid, arch, translate_func, context=context)
return arch
@openerp.tools.ormcache()
@openerp.tools.ormcache('uid', 'id')
def get_view_xmlid(self, cr, uid, id):
imd = self.pool['ir.model.data']
domain = [('model', '=', 'ir.ui.view'), ('res_id', '=', id)]
+2 -2
View File
@@ -337,7 +337,7 @@ class ir_values(osv.osv):
return defaults.values()
# use ormcache: this is called a lot by BaseModel.default_get()!
@tools.ormcache(skiparg=2)
@tools.ormcache('uid', 'model', 'condition')
def get_defaults_dict(self, cr, uid, model, condition=False):
""" Returns a dictionary mapping field names with their corresponding
default value. This method simply improves the returned value of
@@ -387,7 +387,7 @@ class ir_values(osv.osv):
'value': action,
})
@tools.ormcache_context(accepted_keys=('lang',))
@tools.ormcache_context('uid', 'action_slot', 'model', 'res_id', keys=('lang',))
def get_actions(self, cr, uid, action_slot, model, res_id=False, context=None):
"""Retrieves the list of actions bound to the given model's action slot.
See the class description for more details about the various action
+1 -1
View File
@@ -240,7 +240,7 @@ class res_company(osv.osv):
return rule.company_dest_id.id
return user.company_id.id
@tools.ormcache()
@tools.ormcache('uid', 'company')
def _get_company_children(self, cr, uid=None, company=None):
if not company:
return []
+2 -2
View File
@@ -176,13 +176,13 @@ class lang(osv.osv):
(_check_grouping, "The Separator Format should be like [,n] where 0 < n :starting from Unit digit.-1 will end the separation. e.g. [3,2,-1] will represent 106500 to be 1,06,500;[1,2,-1] will represent it to be 106,50,0;[3] will represent it as 106,500. Provided ',' as the thousand separator in each case.", ['grouping'])
]
@tools.ormcache(skiparg=3)
@tools.ormcache('lang')
def _lang_get(self, cr, uid, lang):
lang_ids = self.search(cr, uid, [('code', '=', lang)]) or \
self.search(cr, uid, [('code', '=', 'en_US')])
return lang_ids[0]
@tools.ormcache(skiparg=3)
@tools.ormcache('lang', 'monetary')
def _lang_data_get(self, cr, uid, lang, monetary=False):
if type(lang) in (str, unicode):
lang = self._lang_get(cr, uid, lang)
+5 -8
View File
@@ -344,7 +344,7 @@ class res_users(osv.osv):
for id in ids:
if id in self._uid_cache[db]:
del self._uid_cache[db][id]
self._context_get.clear_cache(self)
self.context_get.clear_cache(self)
self.has_group.clear_cache(self)
return res
@@ -379,9 +379,9 @@ class res_users(osv.osv):
default['login'] = _("%s (copy)") % user2copy['login']
return super(res_users, self).copy(cr, uid, id, default, context)
@tools.ormcache(skiparg=2)
def _context_get(self, cr, uid):
user = self.browse(cr, SUPERUSER_ID, uid)
@tools.ormcache('uid')
def context_get(self, cr, uid, context=None):
user = self.browse(cr, SUPERUSER_ID, uid, context)
result = {}
for k in self._fields:
if k.startswith('context_'):
@@ -397,9 +397,6 @@ class res_users(osv.osv):
result[context_key] = res or False
return result
def context_get(self, cr, uid, context=None):
return self._context_get(cr, uid)
def action_get(self, cr, uid, context=None):
dataobj = self.pool['ir.model.data']
data_id = dataobj._get_id(cr, SUPERUSER_ID, 'base', 'action_res_users_my')
@@ -533,7 +530,7 @@ class res_users(osv.osv):
'target': 'new',
}
@tools.ormcache(skiparg=2)
@tools.ormcache('uid', 'group_ext_id')
def has_group(self, cr, uid, group_ext_id):
"""Checks whether user belongs to given group.
+1
View File
@@ -14,6 +14,7 @@ import test_ir_values
import test_mail
import test_menu
import test_orm
import test_ormcache
import test_osv
import test_qweb
import test_res_config
@@ -0,0 +1,36 @@
from openerp.tests import common
from openerp.tools import get_cache_key_counter
class TestOrmcache(common.TransactionCase):
def test_ormcache(self):
""" Test the effectiveness of the ormcache() decorator. """
IMD = self.env['ir.model.data']
XMLID = 'base.group_no_one'
# retrieve the cache, its key and stat counter
cache, key, counter = get_cache_key_counter(IMD.xmlid_lookup, self.cr, self.uid, XMLID)
hit = counter.hit
miss = counter.miss
# clear the cache of ir.model.data.xmlid_lookup, retrieve its key and
IMD.xmlid_lookup.clear_cache(IMD)
self.assertNotIn(key, cache)
# lookup some reference
self.env.ref(XMLID)
self.assertEqual(counter.hit, hit)
self.assertEqual(counter.miss, miss + 1)
self.assertIn(key, cache)
# lookup again
self.env.ref(XMLID)
self.assertEqual(counter.hit, hit + 1)
self.assertEqual(counter.miss, miss + 1)
self.assertIn(key, cache)
# lookup again
self.env.ref(XMLID)
self.assertEqual(counter.hit, hit + 2)
self.assertEqual(counter.miss, miss + 1)
self.assertIn(key, cache)
+1 -5
View File
@@ -199,13 +199,9 @@ class Registry(Mapping):
This clears the caches associated to methods decorated with
``tools.ormcache`` or ``tools.ormcache_multi`` for all the models.
"""
self.cache.clear()
for model in self.models.itervalues():
model.clear_caches()
# Special case for ir_ui_menu which does not use openerp.tools.ormcache.
ir_ui_menu = self.models.get('ir.ui.menu')
if ir_ui_menu is not None:
ir_ui_menu.clear_cache()
# Useful only in a multi-process context.
def reset_any_cache_cleared(self):
+93 -46
View File
@@ -23,7 +23,7 @@
# this is important for the openerp.api.guess() that relies on signatures
from collections import defaultdict
from decorator import decorator
from inspect import getargspec
from inspect import formatargspec, getargspec
import logging
_logger = logging.getLogger(__name__)
@@ -47,24 +47,53 @@ STAT = defaultdict(ormcache_counter)
class ormcache(object):
""" LRU cache decorator for orm methods. """
""" LRU cache decorator for model methods.
The parameters are strings that represent expressions referring to the
signature of the decorated method, and are used to compute a cache key::
def __init__(self, skiparg=2, size=8192, multi=None, timeout=None):
self.skiparg = skiparg
@ormcache('model_name', 'mode')
def _compute_domain(self, cr, uid, model_name, mode="read"):
...
For the sake of backward compatibility, the decorator supports the named
parameter `skiparg`::
@ormcache(skiparg=3)
def _compute_domain(self, cr, uid, model_name, mode="read"):
...
"""
def __init__(self, *args, **kwargs):
self.args = args
self.skiparg = kwargs.get('skiparg')
def __call__(self, method):
self.method = method
self.determine_key()
lookup = decorator(self.lookup, method)
lookup.clear_cache = self.clear
return lookup
def determine_key(self):
""" Determine the function that computes a cache key from arguments. """
if self.skiparg is None:
# build a string that represents function code and evaluate it
args = formatargspec(*getargspec(self.method))[1:-1]
if self.args:
code = "lambda %s: (%s,)" % (args, ", ".join(self.args))
else:
code = "lambda %s: ()" % (args,)
self.key = eval(code)
else:
# backward-compatible function that uses self.skiparg
self.key = lambda *args, **kwargs: args[self.skiparg:]
def lru(self, model):
counter = STAT[(model.pool.db_name, model._name, self.method)]
return model.pool.cache, (model._name, self.method), counter
def lookup(self, method, *args, **kwargs):
d, key0, counter = self.lru(args[0])
key = key0 + args[self.skiparg:]
key = key0 + self.key(*args, **kwargs)
try:
r = d[key]
counter.hit += 1
@@ -89,53 +118,60 @@ class ormcache(object):
class ormcache_context(ormcache):
def __init__(self, skiparg=2, size=8192, accepted_keys=()):
super(ormcache_context,self).__init__(skiparg,size)
self.accepted_keys = accepted_keys
""" This LRU cache decorator is a variant of :class:`ormcache`, with an
extra parameter ``keys`` that defines a sequence of dictionary keys. Those
keys are looked up in the ``context`` parameter and combined to the cache
key made by :class:`ormcache`.
"""
def __init__(self, *args, **kwargs):
super(ormcache_context, self).__init__(*args, **kwargs)
self.keys = kwargs['keys']
def __call__(self, method):
# remember which argument is context
args = getargspec(method)[0]
self.context_pos = args.index('context')
return super(ormcache_context, self).__call__(method)
def lookup(self, method, *args, **kwargs):
d, key0, counter = self.lru(args[0])
# Note. The decorator() wrapper (used in __call__ above) will resolve
# arguments, and pass them positionally to lookup(). This is why context
# is not passed through kwargs!
if self.context_pos < len(args):
context = args[self.context_pos] or {}
def determine_key(self):
""" Determine the function that computes a cache key from arguments. """
assert self.skiparg is None, "ormcache_context() no longer supports skiparg"
# build a string that represents function code and evaluate it
spec = getargspec(self.method)
args = formatargspec(*spec)[1:-1]
cont_expr = "(context or {})" if 'context' in spec.args else "self._context"
keys_expr = "tuple(map(%s.get, %r))" % (cont_expr, self.keys)
if self.args:
code = "lambda %s: (%s, %s)" % (args, ", ".join(self.args), keys_expr)
else:
context = kwargs.get('context') or {}
ckey = [(k, context[k]) for k in self.accepted_keys if k in context]
# Beware: do not take the context from args!
key = key0 + args[self.skiparg:self.context_pos] + tuple(ckey)
try:
r = d[key]
counter.hit += 1
return r
except KeyError:
counter.miss += 1
value = d[key] = self.method(*args, **kwargs)
return value
except TypeError:
counter.err += 1
return self.method(*args, **kwargs)
code = "lambda %s: (%s,)" % (args, keys_expr)
self.key = eval(code)
class ormcache_multi(ormcache):
def __init__(self, skiparg=2, size=8192, multi=3):
assert skiparg <= multi
super(ormcache_multi, self).__init__(skiparg, size)
self.multi = multi
""" This LRU cache decorator is a variant of :class:`ormcache`, with an
extra parameter ``multi`` that gives the name of a parameter. Upon call, the
corresponding argument is iterated on, and every value leads to a cache
entry under its own key.
"""
def __init__(self, *args, **kwargs):
super(ormcache_multi, self).__init__(*args, **kwargs)
self.multi = kwargs['multi']
def determine_key(self):
""" Determine the function that computes a cache key from arguments. """
assert self.skiparg is None, "ormcache_multi() no longer supports skiparg"
assert isinstance(self.multi, basestring), "ormcache_multi() parameter multi must be an argument name"
super(ormcache_multi, self).determine_key()
# key_multi computes the extra element added to the key
spec = getargspec(self.method)
args = formatargspec(*spec)[1:-1]
code_multi = "lambda %s: %s" % (args, self.multi)
self.key_multi = eval(code_multi)
# self.multi_pos is the position of self.multi in args
self.multi_pos = spec.args.index(self.multi)
def lookup(self, method, *args, **kwargs):
d, key0, counter = self.lru(args[0])
base_key = key0 + args[self.skiparg:self.multi] + args[self.multi+1:]
ids = args[self.multi]
base_key = key0 + self.key(*args, **kwargs)
ids = self.key_multi(*args, **kwargs)
result = {}
missed = []
@@ -150,9 +186,11 @@ class ormcache_multi(ormcache):
missed.append(i)
if missed:
# call the method for the ids that were not in the cache
# call the method for the ids that were not in the cache; note that
# thanks to decorator(), the multi argument will be bound and passed
# positionally in args.
args = list(args)
args[self.multi] = missed
args[self.multi_pos] = missed
result.update(method(*args, **kwargs))
# store those new results back in the cache
@@ -196,5 +234,14 @@ def log_ormcache_stats(sig=None, frame=None):
me.dbname = me_dbname
def get_cache_key_counter(bound_method, *args, **kwargs):
""" Return the cache, key and stat counter for the given call. """
model = bound_method.im_self
ormcache = bound_method.clear_cache.im_self
cache, key0, counter = ormcache.lru(model)
key = key0 + ormcache.key(model, *args, **kwargs)
return cache, key, counter
# For backward compatibility
cache = ormcache