diff --git a/addons/decimal_precision/decimal_precision.py b/addons/decimal_precision/decimal_precision.py index a33d76be9e6..5e87bf18f2a 100644 --- a/addons/decimal_precision/decimal_precision.py +++ b/addons/decimal_precision/decimal_precision.py @@ -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 diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index c14b2328b43..f8b52978192 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -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)] diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 8006b0a49db..6fba655157d 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -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: diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index a6d40bd5654..0214e9246e4 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -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) diff --git a/addons/website_version/models/ir_ui_view.py b/addons/website_version/models/ir_ui_view.py index 2c9ab3c7231..94fa6cab45e 100644 --- a/addons/website_version/models/ir_ui_view.py +++ b/addons/website_version/models/ir_ui_view.py @@ -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) diff --git a/openerp/addons/base/ir/ir_config_parameter.py b/openerp/addons/base/ir/ir_config_parameter.py index c47e9ddd659..0beaa1c0b08 100644 --- a/openerp/addons/base/ir/ir_config_parameter.py +++ b/openerp/addons/base/ir/ir_config_parameter.py @@ -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: diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index bf9c35bdaec..4fbcb32b3e4 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -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 diff --git a/openerp/addons/base/ir/ir_rule.py b/openerp/addons/base/ir/ir_rule.py index c54bb5c28ed..bf95ccc5e3e 100644 --- a/openerp/addons/base/ir/ir_rule.py +++ b/openerp/addons/base/ir/ir_rule.py @@ -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 diff --git a/openerp/addons/base/ir/ir_translation.py b/openerp/addons/base/ir/ir_translation.py index 090e31dbc15..1d25678c06e 100644 --- a/openerp/addons/base/ir/ir_translation.py +++ b/openerp/addons/base/ir/ir_translation.py @@ -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) diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py index b9acb5961d9..2e0f0060e61 100644 --- a/openerp/addons/base/ir/ir_ui_menu.py +++ b/openerp/addons/base/ir/ir_ui_menu.py @@ -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). diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 25e2d4cfec4..c2de3225d7f 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -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)] diff --git a/openerp/addons/base/ir/ir_values.py b/openerp/addons/base/ir/ir_values.py index c6cdd4f6233..76eadd39df4 100644 --- a/openerp/addons/base/ir/ir_values.py +++ b/openerp/addons/base/ir/ir_values.py @@ -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 diff --git a/openerp/addons/base/res/res_company.py b/openerp/addons/base/res/res_company.py index 7bda8cd53bb..2bc57e2bcad 100644 --- a/openerp/addons/base/res/res_company.py +++ b/openerp/addons/base/res/res_company.py @@ -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 [] diff --git a/openerp/addons/base/res/res_lang.py b/openerp/addons/base/res/res_lang.py index b9dacf1c2d8..8f6c780ad8d 100644 --- a/openerp/addons/base/res/res_lang.py +++ b/openerp/addons/base/res/res_lang.py @@ -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) diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py index 0f4d4ad2cc4..7e4b651e7eb 100644 --- a/openerp/addons/base/res/res_users.py +++ b/openerp/addons/base/res/res_users.py @@ -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. diff --git a/openerp/addons/base/tests/__init__.py b/openerp/addons/base/tests/__init__.py index f41543cffcd..82b79cfc693 100644 --- a/openerp/addons/base/tests/__init__.py +++ b/openerp/addons/base/tests/__init__.py @@ -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 diff --git a/openerp/addons/base/tests/test_ormcache.py b/openerp/addons/base/tests/test_ormcache.py new file mode 100644 index 00000000000..9d44695a32c --- /dev/null +++ b/openerp/addons/base/tests/test_ormcache.py @@ -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) diff --git a/openerp/modules/registry.py b/openerp/modules/registry.py index 48e36eb1dc3..a6d3e41c0db 100644 --- a/openerp/modules/registry.py +++ b/openerp/modules/registry.py @@ -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): diff --git a/openerp/tools/cache.py b/openerp/tools/cache.py index 7070d437c2d..339c942f3c7 100644 --- a/openerp/tools/cache.py +++ b/openerp/tools/cache.py @@ -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