From 3219a60167b6d5670a6df0d2f5b9944951245322 Mon Sep 17 00:00:00 2001 From: Malcolm Tredinnick Date: Fri, 17 Aug 2007 17:23:15 +0000 Subject: [PATCH] Rewrote portions of the app- and model-cache initialisation to handle some corner cases. It is now possible to use m2m relations before everything is imported and still get the right results later when importing is complete. Also, get_apps() should always return the same results, so apps won't randomly disappear in the admin interface. Also reorganised the structure of loading.py, since the number of global variables was exploding. The public API is still backwards compatible. Fixed #1796 and #2438 (he claims, optimistically). git-svn-id: http://code.djangoproject.com/svn/django/trunk@5919 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/db/models/loading.py | 279 +++++++++++++++++++++++------------- django/db/models/options.py | 5 +- 2 files changed, 180 insertions(+), 104 deletions(-) diff --git a/django/db/models/loading.py b/django/db/models/loading.py index 224f5e8451..dc8e903c45 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -4,113 +4,188 @@ from django.conf import settings from django.core.exceptions import ImproperlyConfigured import sys import os +import threading -__all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models') +__all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models', + 'load_app', 'cache_ready') -_app_list = [] # Cache of installed apps. - # Entry is not placed in app_list cache until entire app is loaded. -_app_models = {} # Dictionary of models against app label - # Each value is a dictionary of model name: model class - # Applabel and Model entry exists in cache when individual model is loaded. -_app_errors = {} # Dictionary of errors that were experienced when loading the INSTALLED_APPS - # Key is the app_name of the model, value is the exception that was raised - # during model loading. -_loaded = False # Has the contents of settings.INSTALLED_APPS been loaded? - # i.e., has get_apps() been called? - -def get_apps(): - "Returns a list of all installed modules that contain models." - global _app_list - global _loaded - if not _loaded: - _loaded = True - for app_name in settings.INSTALLED_APPS: - try: - load_app(app_name) - except Exception, e: - # Problem importing the app - _app_errors[app_name] = e - return _app_list - -def get_app(app_label, emptyOK=False): - "Returns the module containing the models for the given app_label. If the app has no models in it and 'emptyOK' is True, returns None." - get_apps() # Run get_apps() to populate the _app_list cache. Slightly hackish. - for app_name in settings.INSTALLED_APPS: - if app_label == app_name.split('.')[-1]: - mod = load_app(app_name) - if mod is None: - if emptyOK: - return None - else: - return mod - raise ImproperlyConfigured, "App with label %s could not be found" % app_label - -def load_app(app_name): - "Loads the app with the provided fully qualified name, and returns the model module." - global _app_list - mod = __import__(app_name, {}, {}, ['models']) - if not hasattr(mod, 'models'): - return None - if mod.models not in _app_list: - _app_list.append(mod.models) - return mod.models - -def get_app_errors(): - "Returns the map of known problems with the INSTALLED_APPS" - global _app_errors - get_apps() # Run get_apps() to populate the _app_list cache. Slightly hackish. - return _app_errors - -def get_models(app_mod=None): +class Cache(object): """ - Given a module containing models, returns a list of the models. Otherwise - returns a list of all installed models. + A cache that stores installed applications and their models. Used to + provide reverse-relations and for app introspection (e.g. admin). """ - app_list = get_apps() # Run get_apps() to populate the _app_list cache. Slightly hackish. - if app_mod: - return _app_models.get(app_mod.__name__.split('.')[-2], {}).values() - else: - model_list = [] - for app_mod in app_list: - model_list.extend(get_models(app_mod)) - return model_list + # Use the Borg pattern to share state between all instances. Details at + # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66531. + __shared_state = dict( + # Keys of app_store are the model modules for each application. + app_store = {}, -def get_model(app_label, model_name, seed_cache=True): - """ - Returns the model matching the given app_label and case-insensitive - model_name. + # Mapping of app_labels to a dictionary of model names to model code. + app_models = {}, - Returns None if no model is found. - """ - if seed_cache: - get_apps() - try: - model_dict = _app_models[app_label] - except KeyError: - return None + # Mapping of app_labels to errors raised when trying to import the app. + app_errors = {}, - try: - return model_dict[model_name.lower()] - except KeyError: - return None + # -- Everything below here is only used when populating the cache -- + loaded = False, + handled = {}, + postponed = [], + nesting_level = 0, + write_lock = threading.RLock(), + ) -def register_models(app_label, *models): - """ - Register a set of models as belonging to an app. - """ - for model in models: - # Store as 'name: model' pair in a dictionary - # in the _app_models dictionary - model_name = model._meta.object_name.lower() - model_dict = _app_models.setdefault(app_label, {}) - if model_name in model_dict: - # The same model may be imported via different paths (e.g. - # appname.models and project.appname.models). We use the source - # filename as a means to detect identity. - fname1 = os.path.abspath(sys.modules[model.__module__].__file__) - fname2 = os.path.abspath(sys.modules[model_dict[model_name].__module__].__file__) - # Since the filename extension could be .py the first time and .pyc - # or .pyo the second time, ignore the extension when comparing. - if os.path.splitext(fname1)[0] == os.path.splitext(fname2)[0]: - continue - model_dict[model_name] = model + def __init__(self): + self.__dict__ = self.__shared_state + + def _populate(self): + """ + Fill in all the cache information. This method is threadsafe, in the + sense that every caller will see the same state upon return, and if the + cache is already initialised, it does no work. + """ + if self.loaded: + return + self.write_lock.acquire() + try: + if self.loaded: + return + for app_name in settings.INSTALLED_APPS: + if app_name in self.handled: + continue + try: + self.load_app(app_name, True) + except Exception, e: + # Problem importing the app + self.app_errors[app_name] = e + if not self.nesting_level: + for app_name in self.postponed: + self.load_app(app_name) + self.loaded = True + finally: + self.write_lock.release() + + def load_app(self, app_name, can_postpone=False): + """ + Loads the app with the provided fully qualified name, and returns the + model module. + """ + self.handled[app_name] = None + self.nesting_level += 1 + mod = __import__(app_name, {}, {}, ['models']) + self.nesting_level -= 1 + if not hasattr(mod, 'models'): + if can_postpone: + # Either the app has no models, or the package is still being + # imported by Python and the model module isn't available yet. + # We will check again once all the recursion has finished (in + # populate). + self.postponed.append(app_name) + return None + if mod.models not in self.app_store: + self.app_store[mod.models] = len(self.app_store) + return mod.models + + def cache_ready(self): + """ + Returns true if the model cache is fully populated. + + Useful for code that wants to cache the results of get_models() for + themselves once it is safe to do so. + """ + return self.loaded + + def get_apps(self): + "Returns a list of all installed modules that contain models." + self._populate() + + # Ensure the returned list is always in the same order (with new apps + # added at the end). This avoids unstable ordering on the admin app + # list page, for example. + apps = [(v, k) for k, v in self.app_store.items()] + apps.sort() + return [elt[1] for elt in apps] + + def get_app(self, app_label, emptyOK=False): + """ + Returns the module containing the models for the given app_label. If + the app has no models in it and 'emptyOK' is True, returns None. + """ + self._populate() + self.write_lock.acquire() + try: + for app_name in settings.INSTALLED_APPS: + if app_label == app_name.split('.')[-1]: + mod = self.load_app(app_name, False) + if mod is None: + if emptyOK: + return None + else: + return mod + raise ImproperlyConfigured, "App with label %s could not be found" % app_label + finally: + self.write_lock.release() + + def get_app_errors(self): + "Returns the map of known problems with the INSTALLED_APPS." + self._populate() + return self.app_errors + + def get_models(self, app_mod=None): + """ + Given a module containing models, returns a list of the models. + Otherwise returns a list of all installed models. + """ + self._populate() + if app_mod: + return self.app_models.get(app_mod.__name__.split('.')[-2], {}).values() + else: + model_list = [] + for app_entry in self.app_models.itervalues(): + model_list.extend(app_entry.values()) + return model_list + + def get_model(self, app_label, model_name, seed_cache=True): + """ + Returns the model matching the given app_label and case-insensitive + model_name. + + Returns None if no model is found. + """ + if seed_cache: + self._populate() + return self.app_models.get(app_label, {}).get(model_name.lower()) + + def register_models(self, app_label, *models): + """ + Register a set of models as belonging to an app. + """ + for model in models: + # Store as 'name: model' pair in a dictionary + # in the _app_models dictionary + model_name = model._meta.object_name.lower() + model_dict = self.app_models.setdefault(app_label, {}) + if model_name in model_dict: + # The same model may be imported via different paths (e.g. + # appname.models and project.appname.models). We use the source + # filename as a means to detect identity. + fname1 = os.path.abspath(sys.modules[model.__module__].__file__) + fname2 = os.path.abspath(sys.modules[model_dict[model_name].__module__].__file__) + # Since the filename extension could be .py the first time and + # .pyc or .pyo the second time, ignore the extension when + # comparing. + if os.path.splitext(fname1)[0] == os.path.splitext(fname2)[0]: + continue + model_dict[model_name] = model + +cache = Cache() + +# These methods were always module level, so are kept that way for backwards +# compatibility. +get_apps = cache.get_apps +get_app = cache.get_app +get_app_errors = cache.get_app_errors +get_models = cache.get_models +get_model = cache.get_model +register_models = cache.register_models +load_app = cache.load_app +cache_ready = cache.cache_ready diff --git a/django/db/models/options.py b/django/db/models/options.py index 7cccb611cf..499da1eb01 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -2,7 +2,7 @@ from django.conf import settings from django.db.models.related import RelatedObject from django.db.models.fields.related import ManyToManyRel from django.db.models.fields import AutoField, FieldDoesNotExist -from django.db.models.loading import get_models +from django.db.models.loading import get_models, cache_ready from django.db.models.query import orderlist2sql from django.db.models import Manager from django.utils.translation import activate, deactivate_all, get_language, string_concat @@ -179,7 +179,8 @@ class Options(object): for f in klass._meta.many_to_many: if f.rel and self == f.rel.to._meta: rel_objs.append(RelatedObject(f.rel.to, klass, f)) - self._all_related_many_to_many_objects = rel_objs + if cache_ready(): + self._all_related_many_to_many_objects = rel_objs return rel_objs def get_ordered_objects(self):