class Registry(Mapping): """ Model registry for a particular database. The registry is essentially a mapping between model names and model instances. There is one registry instance per database. """ def __init__(self, db_name): super(Registry, self).__init__() self.models = {} # model name/model instance mapping self._sql_error = {} self._store_function = {} self._pure_function_fields = {} # {model: [field, ...], ...} self._init = True self._init_parent = {} self._assertion_report = assertion_report.assertion_report() self._fields_by_model = None # modules fully loaded (maintained during init phase by `loading` module) self._init_modules = set() self.db_name = db_name self._db = openerp.sql_db.db_connect(db_name) # special cursor for test mode; None means "normal" mode self.test_cr = None # Indicates that the registry is self.ready = False # Inter-process signaling (used only when openerp.multi_process is True): # The `base_registry_signaling` sequence indicates the whole registry # must be reloaded. # The `base_cache_signaling sequence` indicates all caches must be # invalidated (i.e. cleared). self.base_registry_signaling_sequence = None self.base_cache_signaling_sequence = None self.cache = LRU(8192) # Flag indicating if at least one model cache has been cleared. # Useful only in a multi-process context. self._any_cache_cleared = False cr = self.cursor() has_unaccent = openerp.modules.db.has_unaccent(cr) if openerp.tools.config['unaccent'] and not has_unaccent: _logger.warning( "The option --unaccent was given but no unaccent() function was found in database." ) self.has_unaccent = openerp.tools.config['unaccent'] and has_unaccent cr.close() # # Mapping abstract methods implementation # => mixin provides methods keys, items, values, get, __eq__, and __ne__ # def __len__(self): """ Return the size of the registry. """ return len(self.models) def __iter__(self): """ Return an iterator over all model names. """ return iter(self.models) def __getitem__(self, model_name): """ Return the model with the given name or raise KeyError if it doesn't exist.""" return self.models[model_name] def __call__(self, model_name): """ Same as ``self[model_name]``. """ return self.models[model_name] @lazy_property def model_cache(self): return RegistryManager.model_cache @lazy_property def pure_function_fields(self): """ Return the list of pure function fields (field objects) """ fields = [] for mname, fnames in self._pure_function_fields.iteritems(): model_fields = self[mname]._fields for fname in fnames: fields.append(model_fields[fname]) return fields @lazy_property def field_sequence(self): """ Return a function mapping a field to an integer. The value of a field is guaranteed to be strictly greater than the value of the field's dependencies. """ # map fields on their dependents dependents = { field: set(dep for dep, _ in model._field_triggers[field] if dep != field) for model in self.itervalues() for field in model._fields.itervalues() } # sort them topologically, and associate a sequence number to each field mapping = { field: num for num, field in enumerate(reversed(topological_sort(dependents))) } return mapping.get def clear_manual_fields(self): """ Invalidate the cache for manual fields. """ self._fields_by_model = None def get_manual_fields(self, cr, model_name): """ Return the manual fields (as a dict) for the given model. """ if self._fields_by_model is None: # Query manual fields for all models at once self._fields_by_model = dic = defaultdict(dict) cr.execute('SELECT * FROM ir_model_fields WHERE state=%s', ('manual', )) for field in cr.dictfetchall(): dic[field['model']][field['name']] = field return self._fields_by_model[model_name] def do_parent_store(self, cr): for model in self._init_parent: if model in self: self[model]._parent_store_compute(cr) self._init = False def obj_list(self): """ Return the list of model names in this registry.""" return self.keys() def add(self, model_name, model): """ Add or replace a model in the registry.""" self.models[model_name] = model def load(self, cr, module): """ Load a given module in the registry. At the Python level, the modules are already loaded, but not yet on a per-registry level. This method populates a registry with the given modules, i.e. it instanciates all the classes of a the given module and registers them in the registry. """ from .. import models loaded_models = OrderedSet() def mark_loaded(model): # recursively mark model and its children loaded_models.add(model._name) for child_name in model._inherit_children: mark_loaded(self[child_name]) lazy_property.reset_all(self) # Instantiate registered classes (via the MetaModel automatic discovery # or via explicit constructor call), and add them to the pool. for cls in models.MetaModel.module_to_models.get(module.name, []): # models register themselves in self.models model = cls._build_model(self, cr) mark_loaded(model) return map(self, loaded_models) def setup_models(self, cr, partial=False): """ Complete the setup of models. This must be called after loading modules and before using the ORM. :param partial: ``True`` if all models have not been loaded yet. """ lazy_property.reset_all(self) # load custom models ir_model = self['ir.model'] cr.execute('SELECT * FROM ir_model WHERE state=%s', ('manual', )) for model_data in cr.dictfetchall(): ir_model._instanciate(cr, SUPERUSER_ID, model_data, {}) # prepare the setup on all models for model in self.models.itervalues(): model._prepare_setup(cr, SUPERUSER_ID) # do the actual setup from a clean state self._m2m = {} for model in self.models.itervalues(): model._setup_base(cr, SUPERUSER_ID, partial) for model in self.models.itervalues(): model._setup_fields(cr, SUPERUSER_ID) for model in self.models.itervalues(): model._setup_complete(cr, SUPERUSER_ID) def clear_caches(self): """ Clear the caches 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() # Useful only in a multi-process context. def reset_any_cache_cleared(self): self._any_cache_cleared = False # Useful only in a multi-process context. def any_cache_cleared(self): return self._any_cache_cleared @classmethod def setup_multi_process_signaling(cls, cr): if not openerp.multi_process: return None, None # Inter-process signaling: # The `base_registry_signaling` sequence indicates the whole registry # must be reloaded. # The `base_cache_signaling sequence` indicates all caches must be # invalidated (i.e. cleared). cr.execute( """SELECT sequence_name FROM information_schema.sequences WHERE sequence_name='base_registry_signaling'""" ) if not cr.fetchall(): cr.execute( """CREATE SEQUENCE base_registry_signaling INCREMENT BY 1 START WITH 1""" ) cr.execute("""SELECT nextval('base_registry_signaling')""") cr.execute( """CREATE SEQUENCE base_cache_signaling INCREMENT BY 1 START WITH 1""" ) cr.execute("""SELECT nextval('base_cache_signaling')""") cr.execute(""" SELECT base_registry_signaling.last_value, base_cache_signaling.last_value FROM base_registry_signaling, base_cache_signaling""") r, c = cr.fetchone() _logger.debug("Multiprocess load registry signaling: [Registry: # %s] "\ "[Cache: # %s]", r, c) return r, c def in_test_mode(self): """ Test whether the registry is in 'test' mode. """ return self.test_cr is not None def enter_test_mode(self): """ Enter the 'test' mode, where one cursor serves several requests. """ assert self.test_cr is None self.test_cr = self._db.test_cursor() RegistryManager.enter_test_mode() def leave_test_mode(self): """ Leave the test mode. """ assert self.test_cr is not None self.clear_caches() self.test_cr.force_close() self.test_cr = None RegistryManager.leave_test_mode() def cursor(self): """ Return a new cursor for the database. The cursor itself may be used as a context manager to commit/rollback and close automatically. """ cr = self.test_cr if cr is not None: # While in test mode, we use one special cursor across requests. The # test cursor uses a reentrant lock to serialize accesses. The lock # is granted here by cursor(), and automatically released by the # cursor itself in its method close(). cr.acquire() return cr return self._db.cursor()
class Registry(Mapping): """ Model registry for a particular database. The registry is essentially a mapping between model names and model classes. There is one registry instance per database. """ _lock = threading.RLock() _saved_lock = None @lazy_classproperty def registries(cls): """ A mapping from database names to registries. """ size = config.get('registry_lru_size', None) if not size: # Size the LRU depending of the memory limits if os.name != 'posix': # cannot specify the memory limit soft on windows... size = 42 else: # A registry takes 10MB of memory on average, so we reserve # 10Mb (registry) + 5Mb (working memory) per registry avgsz = 15 * 1024 * 1024 size = int(config['limit_memory_soft'] / avgsz) return LRU(size) def __new__(cls, db_name): """ Return the registry for the given database name.""" with cls._lock: try: return cls.registries[db_name] except KeyError: return cls.new(db_name) finally: # set db tracker - cleaned up at the WSGI dispatching phase in # openerp.service.wsgi_server.application threading.current_thread().dbname = db_name @classmethod def new(cls, db_name, force_demo=False, status=None, update_module=False): """ Create and return a new registry for the given database name. """ with cls._lock: with openerp.api.Environment.manage(): registry = object.__new__(cls) registry.init(db_name) # Initializing a registry will call general code which will in # turn call Registry() to obtain the registry being initialized. # Make it available in the registries dictionary then remove it # if an exception is raised. cls.delete(db_name) cls.registries[db_name] = registry try: registry.setup_signaling() # This should be a method on Registry openerp.modules.load_modules(registry._db, force_demo, status, update_module) except Exception: _logger.exception('Failed to load registry') del cls.registries[db_name] raise # load_modules() above can replace the registry by calling # indirectly new() again (when modules have to be uninstalled). # Yeah, crazy. init_parent = registry._init_parent registry = cls.registries[db_name] registry._init_parent.update(init_parent) with closing(registry.cursor()) as cr: registry.do_parent_store(cr) cr.commit() registry.ready = True if update_module: # only in case of update, otherwise we'll have an infinite reload loop! registry.signal_registry_change() return registry def init(self, db_name): self.models = {} # model name/model instance mapping self._sql_error = {} self._init = True self._init_parent = {} self._assertion_report = assertion_report.assertion_report() self._fields_by_model = None # modules fully loaded (maintained during init phase by `loading` module) self._init_modules = set() self.db_name = db_name self._db = openerp.sql_db.db_connect(db_name) # special cursor for test mode; None means "normal" mode self.test_cr = None # Indicates that the registry is self.ready = False # Inter-process signaling (used only when openerp.multi_process is True): # The `base_registry_signaling` sequence indicates the whole registry # must be reloaded. # The `base_cache_signaling sequence` indicates all caches must be # invalidated (i.e. cleared). self.registry_sequence = None self.cache_sequence = None self.cache = LRU(8192) # Flag indicating if at least one model cache has been cleared. # Useful only in a multi-process context. self.cache_cleared = False with closing(self.cursor()) as cr: has_unaccent = openerp.modules.db.has_unaccent(cr) if openerp.tools.config['unaccent'] and not has_unaccent: _logger.warning("The option --unaccent was given but no unaccent() function was found in database.") self.has_unaccent = openerp.tools.config['unaccent'] and has_unaccent @classmethod def delete(cls, db_name): """ Delete the registry linked to a given database. """ with cls._lock: if db_name in cls.registries: cls.registries[db_name].clear_caches() del cls.registries[db_name] @classmethod def delete_all(cls): """ Delete all the registries. """ with cls._lock: for db_name in cls.registries.keys(): cls.delete(db_name) # # Mapping abstract methods implementation # => mixin provides methods keys, items, values, get, __eq__, and __ne__ # def __len__(self): """ Return the size of the registry. """ return len(self.models) def __iter__(self): """ Return an iterator over all model names. """ return iter(self.models) def __getitem__(self, model_name): """ Return the model with the given name or raise KeyError if it doesn't exist.""" return self.models[model_name] def __call__(self, model_name): """ Same as ``self[model_name]``. """ return self.models[model_name] def __setitem__(self, model_name, model): """ Add or replace a model in the registry.""" self.models[model_name] = model @lazy_classproperty def model_cache(cls): """ A cache for model classes, indexed by their base classes. """ # we cache 256 classes per registry on average return LRU(cls.registries.count * 256) @lazy_property def field_sequence(self): """ Return a function mapping a field to an integer. The value of a field is guaranteed to be strictly greater than the value of the field's dependencies. """ # map fields on their dependents dependents = { field: set(dep for dep, _ in model._field_triggers[field] if dep != field) for model in self.itervalues() for field in model._fields.itervalues() } # sort them topologically, and associate a sequence number to each field mapping = { field: num for num, field in enumerate(reversed(topological_sort(dependents))) } return mapping.get def clear_manual_fields(self): """ Invalidate the cache for manual fields. """ self._fields_by_model = None def get_manual_fields(self, cr, model_name): """ Return the manual fields (as a dict) for the given model. """ if self._fields_by_model is None: # Query manual fields for all models at once self._fields_by_model = dic = defaultdict(dict) cr.execute('SELECT * FROM ir_model_fields WHERE state=%s', ('manual',)) for field in cr.dictfetchall(): dic[field['model']][field['name']] = field return self._fields_by_model[model_name] def do_parent_store(self, cr): env = openerp.api.Environment(cr, SUPERUSER_ID, {}) for model_name in self._init_parent: if model_name in env: env[model_name]._parent_store_compute() self._init = False def load(self, cr, module): """ Load a given module in the registry, and return the names of the modified models. At the Python level, the modules are already loaded, but not yet on a per-registry level. This method populates a registry with the given modules, i.e. it instanciates all the classes of a the given module and registers them in the registry. """ from .. import models loaded_models = OrderedSet() def mark_loaded(model): # recursively mark model and its children loaded_models.add(model._name) for child_name in model._inherit_children: mark_loaded(self[child_name]) lazy_property.reset_all(self) # Instantiate registered classes (via the MetaModel automatic discovery # or via explicit constructor call), and add them to the pool. for cls in models.MetaModel.module_to_models.get(module.name, []): # models register themselves in self.models model = cls._build_model(self, cr) mark_loaded(model) return list(loaded_models) def setup_models(self, cr, partial=False): """ Complete the setup of models. This must be called after loading modules and before using the ORM. :param partial: ``True`` if all models have not been loaded yet. """ lazy_property.reset_all(self) env = openerp.api.Environment(cr, SUPERUSER_ID, {}) # load custom models ir_model = env['ir.model'] cr.execute('SELECT * FROM ir_model WHERE state=%s', ('manual',)) for model_data in cr.dictfetchall(): ir_model._instanciate(model_data) # prepare the setup on all models models = env.values() for model in models: model._prepare_setup() # do the actual setup from a clean state self._m2m = {} for model in models: model._setup_base(partial) for model in models: model._setup_fields(partial) for model in models: model._setup_complete() def init_models(self, cr, model_names, context): """ Initialize a list of models (given by their name). Call methods ``_auto_init``, ``init``, and ``_auto_end`` on each model to create or update the database tables supporting the models. The ``context`` may contain the following items: - ``module``: the name of the module being installed/updated, if any; - ``update_custom_fields``: whether custom fields should be updated. """ if 'module' in context: _logger.info('module %s: creating or updating database tables', context['module']) context = dict(context, todo=[]) env = openerp.api.Environment(cr, SUPERUSER_ID, context) models = [env[model_name] for model_name in model_names] for model in models: model._auto_init() model.init() cr.commit() for model in models: model._auto_end() cr.commit() for _, func, args in sorted(context['todo']): func(*args) if models: models[0].recompute() cr.commit() def clear_caches(self): """ Clear 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() def setup_signaling(self): """ Setup the inter-process signaling on this registry. """ if not openerp.multi_process: return with self.cursor() as cr: # The `base_registry_signaling` sequence indicates when the registry # must be reloaded. # The `base_cache_signaling` sequence indicates when all caches must # be invalidated (i.e. cleared). cr.execute("SELECT sequence_name FROM information_schema.sequences WHERE sequence_name='base_registry_signaling'") if not cr.fetchall(): cr.execute("CREATE SEQUENCE base_registry_signaling INCREMENT BY 1 START WITH 1") cr.execute("SELECT nextval('base_registry_signaling')") cr.execute("CREATE SEQUENCE base_cache_signaling INCREMENT BY 1 START WITH 1") cr.execute("SELECT nextval('base_cache_signaling')") cr.execute(""" SELECT base_registry_signaling.last_value, base_cache_signaling.last_value FROM base_registry_signaling, base_cache_signaling""") self.registry_sequence, self.cache_sequence = cr.fetchone() _logger.debug("Multiprocess load registry signaling: [Registry: %s] [Cache: %s]", self.registry_sequence, self.cache_sequence) def check_signaling(self): """ Check whether the registry has changed, and performs all necessary operations to update the registry. Return an up-to-date registry. """ if not openerp.multi_process: return self with closing(self.cursor()) as cr: cr.execute(""" SELECT base_registry_signaling.last_value, base_cache_signaling.last_value FROM base_registry_signaling, base_cache_signaling""") r, c = cr.fetchone() _logger.debug("Multiprocess signaling check: [Registry - %s -> %s] [Cache - %s -> %s]", self.registry_sequence, r, self.cache_sequence, c) # Check if the model registry must be reloaded if self.registry_sequence != r: _logger.info("Reloading the model registry after database signaling.") self = Registry.new(self.db_name) # Check if the model caches must be invalidated. elif self.cache_sequence != c: _logger.info("Invalidating all model caches after database signaling.") self.clear_caches() self.cache_cleared = False self.registry_sequence = r self.cache_sequence = c return self def signal_registry_change(self): """ Notifies other processes that the registry has changed. """ if openerp.multi_process: _logger.info("Registry changed, signaling through the database") with closing(self.cursor()) as cr: cr.execute("select nextval('base_registry_signaling')") self.registry_sequence = cr.fetchone()[0] def signal_caches_change(self): """ Notifies other processes if caches have been invalidated. """ if openerp.multi_process and self.cache_cleared: # signal it through the database to other processes _logger.info("At least one model cache has been invalidated, signaling through the database.") with closing(self.cursor()) as cr: cr.execute("select nextval('base_cache_signaling')") self.cache_sequence = cr.fetchone()[0] self.cache_cleared = False def in_test_mode(self): """ Test whether the registry is in 'test' mode. """ return self.test_cr is not None def enter_test_mode(self): """ Enter the 'test' mode, where one cursor serves several requests. """ assert self.test_cr is None self.test_cr = self._db.test_cursor() assert Registry._saved_lock is None Registry._saved_lock = Registry._lock Registry._lock = DummyRLock() def leave_test_mode(self): """ Leave the test mode. """ assert self.test_cr is not None self.clear_caches() self.test_cr.force_close() self.test_cr = None assert Registry._saved_lock is not None Registry._lock = Registry._saved_lock Registry._saved_lock = None def cursor(self): """ Return a new cursor for the database. The cursor itself may be used as a context manager to commit/rollback and close automatically. """ cr = self.test_cr if cr is not None: # While in test mode, we use one special cursor across requests. The # test cursor uses a reentrant lock to serialize accesses. The lock # is granted here by cursor(), and automatically released by the # cursor itself in its method close(). cr.acquire() return cr return self._db.cursor()
class Registry(Mapping): """ Model registry for a particular database. The registry is essentially a mapping between model names and model instances. There is one registry instance per database. """ def __init__(self, db_name): super(Registry, self).__init__() self.models = {} # model name/model instance mapping self._sql_error = {} self._store_function = {} self._pure_function_fields = {} # {model: [field, ...], ...} self._init = True self._init_parent = {} self._assertion_report = assertion_report.assertion_report() self._fields_by_model = None # modules fully loaded (maintained during init phase by `loading` module) self._init_modules = set() self.db_name = db_name self._db = openerp.sql_db.db_connect(db_name) # special cursor for test mode; None means "normal" mode self.test_cr = None # Indicates that the registry is self.ready = False # Inter-process signaling (used only when openerp.multi_process is True): # The `base_registry_signaling` sequence indicates the whole registry # must be reloaded. # The `base_cache_signaling sequence` indicates all caches must be # invalidated (i.e. cleared). self.base_registry_signaling_sequence = None self.base_cache_signaling_sequence = None self.cache = LRU(8192) # Flag indicating if at least one model cache has been cleared. # Useful only in a multi-process context. self._any_cache_cleared = False cr = self.cursor() has_unaccent = openerp.modules.db.has_unaccent(cr) if openerp.tools.config['unaccent'] and not has_unaccent: _logger.warning("The option --unaccent was given but no unaccent() function was found in database.") self.has_unaccent = openerp.tools.config['unaccent'] and has_unaccent cr.close() # # Mapping abstract methods implementation # => mixin provides methods keys, items, values, get, __eq__, and __ne__ # def __len__(self): """ Return the size of the registry. """ return len(self.models) def __iter__(self): """ Return an iterator over all model names. """ return iter(self.models) def __getitem__(self, model_name): """ Return the model with the given name or raise KeyError if it doesn't exist.""" return self.models[model_name] def __call__(self, model_name): """ Same as ``self[model_name]``. """ return self.models[model_name] @lazy_property def pure_function_fields(self): """ Return the list of pure function fields (field objects) """ fields = [] for mname, fnames in self._pure_function_fields.iteritems(): model_fields = self[mname]._fields for fname in fnames: fields.append(model_fields[fname]) return fields def clear_manual_fields(self): """ Invalidate the cache for manual fields. """ self._fields_by_model = None def get_manual_fields(self, cr, model_name): """ Return the manual fields (as a dict) for the given model. """ if self._fields_by_model is None: # Query manual fields for all models at once self._fields_by_model = dic = defaultdict(dict) cr.execute('SELECT * FROM ir_model_fields WHERE state=%s', ('manual',)) for field in cr.dictfetchall(): dic[field['model']][field['name']] = field return self._fields_by_model[model_name] def do_parent_store(self, cr): for o in self._init_parent: self.get(o)._parent_store_compute(cr) self._init = False def obj_list(self): """ Return the list of model names in this registry.""" return self.keys() def add(self, model_name, model): """ Add or replace a model in the registry.""" self.models[model_name] = model def load(self, cr, module): """ Load a given module in the registry. At the Python level, the modules are already loaded, but not yet on a per-registry level. This method populates a registry with the given modules, i.e. it instanciates all the classes of a the given module and registers them in the registry. """ from .. import models models_to_load = [] # need to preserve loading order lazy_property.reset_all(self) # Instantiate registered classes (via the MetaModel automatic discovery # or via explicit constructor call), and add them to the pool. for cls in models.MetaModel.module_to_models.get(module.name, []): # models register themselves in self.models model = cls._build_model(self, cr) if model._name not in models_to_load: # avoid double-loading models whose declaration is split models_to_load.append(model._name) return [self.models[m] for m in models_to_load] def setup_models(self, cr, partial=False): """ Complete the setup of models. This must be called after loading modules and before using the ORM. :param partial: ``True`` if all models have not been loaded yet. """ lazy_property.reset_all(self) # load custom models ir_model = self['ir.model'] cr.execute('select model, transient from ir_model where state=%s', ('manual',)) for (model_name, transient) in cr.fetchall(): ir_model.instanciate(cr, SUPERUSER_ID, model_name, transient, {}) # prepare the setup on all models for model in self.models.itervalues(): model._prepare_setup(cr, SUPERUSER_ID) # do the actual setup from a clean state self._m2m = {} for model in self.models.itervalues(): model._setup_base(cr, SUPERUSER_ID, partial) for model in self.models.itervalues(): model._setup_fields(cr, SUPERUSER_ID) for model in self.models.itervalues(): model._setup_complete(cr, SUPERUSER_ID) def clear_caches(self): """ Clear the caches 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() # Useful only in a multi-process context. def reset_any_cache_cleared(self): self._any_cache_cleared = False # Useful only in a multi-process context. def any_cache_cleared(self): return self._any_cache_cleared @classmethod def setup_multi_process_signaling(cls, cr): if not openerp.multi_process: return None, None # Inter-process signaling: # The `base_registry_signaling` sequence indicates the whole registry # must be reloaded. # The `base_cache_signaling sequence` indicates all caches must be # invalidated (i.e. cleared). cr.execute("""SELECT sequence_name FROM information_schema.sequences WHERE sequence_name='base_registry_signaling'""") if not cr.fetchall(): cr.execute("""CREATE SEQUENCE base_registry_signaling INCREMENT BY 1 START WITH 1""") cr.execute("""SELECT nextval('base_registry_signaling')""") cr.execute("""CREATE SEQUENCE base_cache_signaling INCREMENT BY 1 START WITH 1""") cr.execute("""SELECT nextval('base_cache_signaling')""") cr.execute(""" SELECT base_registry_signaling.last_value, base_cache_signaling.last_value FROM base_registry_signaling, base_cache_signaling""") r, c = cr.fetchone() _logger.debug("Multiprocess load registry signaling: [Registry: # %s] "\ "[Cache: # %s]", r, c) return r, c def enter_test_mode(self): """ Enter the 'test' mode, where one cursor serves several requests. """ assert self.test_cr is None self.test_cr = self._db.test_cursor() RegistryManager.enter_test_mode() def leave_test_mode(self): """ Leave the test mode. """ assert self.test_cr is not None self.clear_caches() self.test_cr.force_close() self.test_cr = None RegistryManager.leave_test_mode() def cursor(self): """ Return a new cursor for the database. The cursor itself may be used as a context manager to commit/rollback and close automatically. """ cr = self.test_cr if cr is not None: # While in test mode, we use one special cursor across requests. The # test cursor uses a reentrant lock to serialize accesses. The lock # is granted here by cursor(), and automatically released by the # cursor itself in its method close(). cr.acquire() return cr return self._db.cursor()
class Registry(Mapping): """ Model registry for a particular database. The registry is essentially a mapping between model names and model instances. There is one registry instance per database. """ def __init__(self, db_name): super(Registry, self).__init__() self.models = {} # model name/model instance mapping self._sql_error = {} self._store_function = {} self._pure_function_fields = {} # {model: [field, ...], ...} self._init = True self._init_parent = {} self._assertion_report = assertion_report.assertion_report() self._fields_by_model = None # modules fully loaded (maintained during init phase by `loading` module) self._init_modules = set() self.db_name = db_name self._db = openerp.sql_db.db_connect(db_name) # special cursor for test mode; None means "normal" mode self.test_cr = None # Indicates that the registry is self.ready = False # Inter-process signaling (used only when openerp.multi_process is True): # The `base_registry_signaling` sequence indicates the whole registry # must be reloaded. # The `base_cache_signaling sequence` indicates all caches must be # invalidated (i.e. cleared). self.base_registry_signaling_sequence = None self.base_cache_signaling_sequence = None self.cache = LRU(8192) # Flag indicating if at least one model cache has been cleared. # Useful only in a multi-process context. self._any_cache_cleared = False cr = self.cursor() has_unaccent = openerp.modules.db.has_unaccent(cr) if openerp.tools.config['unaccent'] and not has_unaccent: _logger.warning("The option --unaccent was given but no unaccent() function was found in database.") self.has_unaccent = openerp.tools.config['unaccent'] and has_unaccent cr.close() # # Mapping abstract methods implementation # => mixin provides methods keys, items, values, get, __eq__, and __ne__ # def __len__(self): """ Return the size of the registry. """ return len(self.models) def __iter__(self): """ Return an iterator over all model names. """ return iter(self.models) def __getitem__(self, model_name): """ Return the model with the given name or raise KeyError if it doesn't exist.""" return self.models[model_name] def __call__(self, model_name): """ Same as ``self[model_name]``. """ return self.models[model_name] @lazy_property def model_cache(self): return RegistryManager.model_cache @lazy_property def pure_function_fields(self): """ Return the list of pure function fields (field objects) """ fields = [] for mname, fnames in self._pure_function_fields.iteritems(): model_fields = self[mname]._fields for fname in fnames: fields.append(model_fields[fname]) return fields @lazy_property def field_sequence(self): """ Return a function mapping a field to an integer. The value of a field is guaranteed to be strictly greater than the value of the field's dependencies. """ # map fields on their dependents dependents = { field: set(dep for dep, _ in model._field_triggers[field] if dep != field) for model in self.itervalues() for field in model._fields.itervalues() } # sort them topologically, and associate a sequence number to each field mapping = { field: num for num, field in enumerate(reversed(topological_sort(dependents))) } return mapping.get def clear_manual_fields(self): """ Invalidate the cache for manual fields. """ self._fields_by_model = None def get_manual_fields(self, cr, model_name): """ Return the manual fields (as a dict) for the given model. """ if self._fields_by_model is None: # Query manual fields for all models at once self._fields_by_model = dic = defaultdict(dict) cr.execute('SELECT * FROM ir_model_fields WHERE state=%s', ('manual',)) for field in cr.dictfetchall(): dic[field['model']][field['name']] = field return self._fields_by_model[model_name] def do_parent_store(self, cr): env = openerp.api.Environment(cr, SUPERUSER_ID, {}) for model_name in self._init_parent: if model_name in env: env[model_name]._parent_store_compute() self._init = False def obj_list(self): """ Return the list of model names in this registry.""" return self.keys() def add(self, model_name, model): """ Add or replace a model in the registry.""" self.models[model_name] = model def load(self, cr, module): """ Load a given module in the registry, and return the names of the modified models. At the Python level, the modules are already loaded, but not yet on a per-registry level. This method populates a registry with the given modules, i.e. it instanciates all the classes of a the given module and registers them in the registry. """ from .. import models loaded_models = OrderedSet() def mark_loaded(model): # recursively mark model and its children loaded_models.add(model._name) for child_name in model._inherit_children: mark_loaded(self[child_name]) lazy_property.reset_all(self) # Instantiate registered classes (via the MetaModel automatic discovery # or via explicit constructor call), and add them to the pool. for cls in models.MetaModel.module_to_models.get(module.name, []): # models register themselves in self.models model = cls._build_model(self, cr) mark_loaded(model) return list(loaded_models) def setup_models(self, cr, partial=False): """ Complete the setup of models. This must be called after loading modules and before using the ORM. :param partial: ``True`` if all models have not been loaded yet. """ lazy_property.reset_all(self) env = openerp.api.Environment(cr, SUPERUSER_ID, {}) # load custom models ir_model = env['ir.model'] cr.execute('SELECT * FROM ir_model WHERE state=%s', ('manual',)) for model_data in cr.dictfetchall(): ir_model._instanciate(model_data) # prepare the setup on all models models = [env[model_name] for model_name in self.models] for model in models: model._prepare_setup() # do the actual setup from a clean state self._m2m = {} for model in models: model._setup_base(partial) for model in models: model._setup_fields(partial) for model in models: model._setup_complete() def init_models(self, cr, model_names, context): """ Initialize a list of models (given by their name). Call methods ``_auto_init``, ``init``, and ``_auto_end`` on each model to create or update the database tables supporting the models. The ``context`` may contain the following items: - ``module``: the name of the module being installed/updated, if any; - ``update_custom_fields``: whether custom fields should be updated. """ if 'module' in context: _logger.info('module %s: creating or updating database tables', context['module']) context = dict(context, todo=[]) env = openerp.api.Environment(cr, SUPERUSER_ID, context) models = [env[model_name] for model_name in model_names] for model in models: model._auto_init() model.init() cr.commit() for model in models: model._auto_end() cr.commit() for _, func, args in sorted(context['todo']): func(*args) if models: models[0].recompute() cr.commit() def clear_caches(self): """ Clear the caches 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() # Useful only in a multi-process context. def reset_any_cache_cleared(self): self._any_cache_cleared = False # Useful only in a multi-process context. def any_cache_cleared(self): return self._any_cache_cleared @classmethod def setup_multi_process_signaling(cls, cr): if not openerp.multi_process: return None, None # Inter-process signaling: # The `base_registry_signaling` sequence indicates the whole registry # must be reloaded. # The `base_cache_signaling sequence` indicates all caches must be # invalidated (i.e. cleared). cr.execute("""SELECT sequence_name FROM information_schema.sequences WHERE sequence_name='base_registry_signaling'""") if not cr.fetchall(): cr.execute("""CREATE SEQUENCE base_registry_signaling INCREMENT BY 1 START WITH 1""") cr.execute("""SELECT nextval('base_registry_signaling')""") cr.execute("""CREATE SEQUENCE base_cache_signaling INCREMENT BY 1 START WITH 1""") cr.execute("""SELECT nextval('base_cache_signaling')""") cr.execute(""" SELECT base_registry_signaling.last_value, base_cache_signaling.last_value FROM base_registry_signaling, base_cache_signaling""") r, c = cr.fetchone() _logger.debug("Multiprocess load registry signaling: [Registry: # %s] "\ "[Cache: # %s]", r, c) return r, c def in_test_mode(self): """ Test whether the registry is in 'test' mode. """ return self.test_cr is not None def enter_test_mode(self): """ Enter the 'test' mode, where one cursor serves several requests. """ assert self.test_cr is None self.test_cr = self._db.test_cursor() RegistryManager.enter_test_mode() def leave_test_mode(self): """ Leave the test mode. """ assert self.test_cr is not None self.clear_caches() self.test_cr.force_close() self.test_cr = None RegistryManager.leave_test_mode() def cursor(self): """ Return a new cursor for the database. The cursor itself may be used as a context manager to commit/rollback and close automatically. """ cr = self.test_cr if cr is not None: # While in test mode, we use one special cursor across requests. The # test cursor uses a reentrant lock to serialize accesses. The lock # is granted here by cursor(), and automatically released by the # cursor itself in its method close(). cr.acquire() return cr return self._db.cursor()