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
This commit is contained in:
Malcolm Tredinnick 2007-08-17 17:23:15 +00:00
parent 103fe15efc
commit 3219a60167
2 changed files with 180 additions and 104 deletions

View File

@ -4,78 +4,147 @@ from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
import sys import sys
import os 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. class Cache(object):
# Entry is not placed in app_list cache until entire app is loaded. """
_app_models = {} # Dictionary of models against app label A cache that stores installed applications and their models. Used to
# Each value is a dictionary of model name: model class provide reverse-relations and for app introspection (e.g. admin).
# 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 # Use the Borg pattern to share state between all instances. Details at
# Key is the app_name of the model, value is the exception that was raised # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66531.
# during model loading. __shared_state = dict(
_loaded = False # Has the contents of settings.INSTALLED_APPS been loaded? # Keys of app_store are the model modules for each application.
# i.e., has get_apps() been called? app_store = {},
def get_apps(): # Mapping of app_labels to a dictionary of model names to model code.
"Returns a list of all installed modules that contain models." app_models = {},
global _app_list
global _loaded # Mapping of app_labels to errors raised when trying to import the app.
if not _loaded: app_errors = {},
_loaded = True
for app_name in settings.INSTALLED_APPS: # -- Everything below here is only used when populating the cache --
loaded = False,
handled = {},
postponed = [],
nesting_level = 0,
write_lock = threading.RLock(),
)
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: try:
load_app(app_name) 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: except Exception, e:
# Problem importing the app # Problem importing the app
_app_errors[app_name] = e self.app_errors[app_name] = e
return _app_list 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 get_app(app_label, emptyOK=False): def load_app(self, app_name, can_postpone=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. 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: for app_name in settings.INSTALLED_APPS:
if app_label == app_name.split('.')[-1]: if app_label == app_name.split('.')[-1]:
mod = load_app(app_name) mod = self.load_app(app_name, False)
if mod is None: if mod is None:
if emptyOK: if emptyOK:
return None return None
else: else:
return mod return mod
raise ImproperlyConfigured, "App with label %s could not be found" % app_label raise ImproperlyConfigured, "App with label %s could not be found" % app_label
finally:
self.write_lock.release()
def load_app(app_name): def get_app_errors(self):
"Loads the app with the provided fully qualified name, and returns the model module." "Returns the map of known problems with the INSTALLED_APPS."
global _app_list self._populate()
mod = __import__(app_name, {}, {}, ['models']) return self.app_errors
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(): def get_models(self, app_mod=None):
"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):
""" """
Given a module containing models, returns a list of the models. Otherwise Given a module containing models, returns a list of the models.
returns a list of all installed models. Otherwise returns a list of all installed models.
""" """
app_list = get_apps() # Run get_apps() to populate the _app_list cache. Slightly hackish. self._populate()
if app_mod: if app_mod:
return _app_models.get(app_mod.__name__.split('.')[-2], {}).values() return self.app_models.get(app_mod.__name__.split('.')[-2], {}).values()
else: else:
model_list = [] model_list = []
for app_mod in app_list: for app_entry in self.app_models.itervalues():
model_list.extend(get_models(app_mod)) model_list.extend(app_entry.values())
return model_list return model_list
def get_model(app_label, model_name, seed_cache=True): def get_model(self, app_label, model_name, seed_cache=True):
""" """
Returns the model matching the given app_label and case-insensitive Returns the model matching the given app_label and case-insensitive
model_name. model_name.
@ -83,18 +152,10 @@ def get_model(app_label, model_name, seed_cache=True):
Returns None if no model is found. Returns None if no model is found.
""" """
if seed_cache: if seed_cache:
get_apps() self._populate()
try: return self.app_models.get(app_label, {}).get(model_name.lower())
model_dict = _app_models[app_label]
except KeyError:
return None
try: def register_models(self, app_label, *models):
return model_dict[model_name.lower()]
except KeyError:
return None
def register_models(app_label, *models):
""" """
Register a set of models as belonging to an app. Register a set of models as belonging to an app.
""" """
@ -102,15 +163,29 @@ def register_models(app_label, *models):
# Store as 'name: model' pair in a dictionary # Store as 'name: model' pair in a dictionary
# in the _app_models dictionary # in the _app_models dictionary
model_name = model._meta.object_name.lower() model_name = model._meta.object_name.lower()
model_dict = _app_models.setdefault(app_label, {}) model_dict = self.app_models.setdefault(app_label, {})
if model_name in model_dict: if model_name in model_dict:
# The same model may be imported via different paths (e.g. # The same model may be imported via different paths (e.g.
# appname.models and project.appname.models). We use the source # appname.models and project.appname.models). We use the source
# filename as a means to detect identity. # filename as a means to detect identity.
fname1 = os.path.abspath(sys.modules[model.__module__].__file__) fname1 = os.path.abspath(sys.modules[model.__module__].__file__)
fname2 = os.path.abspath(sys.modules[model_dict[model_name].__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 # Since the filename extension could be .py the first time and
# or .pyo the second time, ignore the extension when comparing. # .pyc or .pyo the second time, ignore the extension when
# comparing.
if os.path.splitext(fname1)[0] == os.path.splitext(fname2)[0]: if os.path.splitext(fname1)[0] == os.path.splitext(fname2)[0]:
continue continue
model_dict[model_name] = model 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

View File

@ -2,7 +2,7 @@ from django.conf import settings
from django.db.models.related import RelatedObject from django.db.models.related import RelatedObject
from django.db.models.fields.related import ManyToManyRel from django.db.models.fields.related import ManyToManyRel
from django.db.models.fields import AutoField, FieldDoesNotExist 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.query import orderlist2sql
from django.db.models import Manager from django.db.models import Manager
from django.utils.translation import activate, deactivate_all, get_language, string_concat from django.utils.translation import activate, deactivate_all, get_language, string_concat
@ -179,6 +179,7 @@ class Options(object):
for f in klass._meta.many_to_many: for f in klass._meta.many_to_many:
if f.rel and self == f.rel.to._meta: if f.rel and self == f.rel.to._meta:
rel_objs.append(RelatedObject(f.rel.to, klass, f)) rel_objs.append(RelatedObject(f.rel.to, klass, f))
if cache_ready():
self._all_related_many_to_many_objects = rel_objs self._all_related_many_to_many_objects = rel_objs
return rel_objs return rel_objs