diff --git a/django/apps/__init__.py b/django/apps/__init__.py
new file mode 100644
index 0000000..ca60d0c
--- /dev/null
+++ b/django/apps/__init__.py
@@ -0,0 +1,22 @@
+from django.apps.base import App
+from django.apps.cache import AppCache
+
+__all__ = (
+    'App', 'find_app', 'get_apps', 'get_app', 'get_app_errors',
+    'get_models', 'get_model', 'register_models', 'load_app',
+    'app_cache_ready'
+)
+
+cache = AppCache()
+
+# These methods were always module level, so are kept that way for backwards
+# compatibility.
+find_app = cache.find_app
+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
+app_cache_ready = cache.app_cache_ready
diff --git a/django/apps/base.py b/django/apps/base.py
new file mode 100644
index 0000000..21990bb
--- /dev/null
+++ b/django/apps/base.py
@@ -0,0 +1,64 @@
+import re
+import sys
+
+from django.apps.options import AppOptions, DEFAULT_NAMES
+
+module_name_re = re.compile(r'_([a-z])')
+
+
+class AppBase(type):
+    """
+    Metaclass for all apps.
+    """
+    def __new__(cls, name, bases, attrs):
+        super_new = super(AppBase, cls).__new__
+        parents = [b for b in bases if isinstance(b, AppBase)]
+        if not parents:
+            # If this isn't a subclass of App, don't do anything special.
+            return super_new(cls, name, bases, attrs)
+        module = attrs.pop('__module__', None)
+        new_class = super_new(cls, name, bases, {'__module__': module})
+        attr_meta = attrs.pop('Meta', None)
+        if not attr_meta:
+            meta = getattr(new_class, 'Meta', None)
+        else:
+            meta = attr_meta
+        app_name = attrs.pop('_name', None)
+        if app_name is None:
+            # Figure out the app_name by looking one level up.
+            # For 'django.contrib.sites.app', this would be 'django.contrib.sites'
+            app_module = sys.modules[new_class.__module__]
+            app_name = app_module.__name__.rsplit('.', 1)[0]
+        new_class.add_to_class('_meta', AppOptions(app_name, meta))
+        # For easier Meta inheritance
+        new_class.add_to_class('Meta', attr_meta)
+        return new_class
+
+    def add_to_class(cls, name, value):
+        if hasattr(value, 'contribute_to_class'):
+            value.contribute_to_class(cls, name)
+        else:
+            setattr(cls, name, value)
+
+
+class App(object):
+    """
+    The base app class to be subclassed for own uses.
+    """
+    __metaclass__ = AppBase
+
+    def __init__(self, **options):
+        for key, value in options.iteritems():
+            if key in DEFAULT_NAMES:
+                setattr(self._meta, key, value)
+            else:
+                setattr(self, key, value)
+
+    def __repr__(self):
+        return '<App: %s>' % self._meta.name
+
+    @classmethod
+    def from_name(cls, name):
+        upper = lambda match: match.group(1).upper()
+        cls_name = module_name_re.sub(upper, name.split('.')[-1])
+        return type(cls_name[0].upper()+cls_name[1:], (cls,), {'_name': name})
diff --git a/django/apps/cache.py b/django/apps/cache.py
new file mode 100644
index 0000000..4973e74
--- /dev/null
+++ b/django/apps/cache.py
@@ -0,0 +1,351 @@
+import sys
+import os
+import threading
+
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.datastructures import SortedDict
+from django.utils.importlib import import_module
+from django.utils.module_loading import module_has_submodule
+
+from django.apps.base import App
+from django.apps.signals import app_loaded, post_apps_loaded
+
+
+def _initialize():
+    """
+    Returns a dictionary to be used as the initial value of the
+    shared state of the app cache.
+    """
+    return {
+        # list of loaded app instances
+        'loaded_apps': [],
+
+        # Mapping of app_labels to a dictionary of model names to model code.
+        'app_models': SortedDict(),
+
+        # -- Everything below here is only used when populating the cache --
+        'loaded': False,
+        'handled': [],
+        'postponed': [],
+        'nesting_level': 0,
+        '_get_models_cache': {},
+    }
+
+
+class AppCache(object):
+    """
+    A cache that stores installed applications and their models. Used to
+    provide reverse-relations and for app introspection (e.g. admin).
+    """
+    # Use the Borg pattern to share state between all instances. Details at
+    # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66531.
+    __shared_state = dict(_initialize(), write_lock=threading.RLock())
+
+    def __init__(self):
+        self.__dict__ = self.__shared_state
+
+    def _reset(self):
+        """
+        Resets the cache to its initial (unseeded) state
+        """
+        # remove imported model modules, so ModelBase.__new__ can register
+        # them with the cache again
+        for app, models in self.app_models.iteritems():
+            for model in models.itervalues():
+                module = model.__module__
+                if module in sys.modules:
+                    del sys.modules[module]
+        self.__class__.__shared_state.update(_initialize())
+
+    def _reload(self):
+        """
+        Reloads the cache
+        """
+        self._reset()
+        self._populate()
+
+    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 isinstance(app_name, (tuple, list)):
+                    app_name, app_kwargs = app_name
+                else:
+                    app_kwargs = {}
+                if app_name in self.handled:
+                    continue
+                self.load_app(app_name, app_kwargs, True)
+            if not self.nesting_level:
+                for app_name, app_kwargs in self.postponed:
+                    self.load_app(app_name, app_kwargs)
+                # assign models to app instances
+                for app in self.loaded_apps:
+                    parents = [p for p in app.__class__.mro()
+                               if hasattr(p, '_meta')]
+                    for parent in reversed(parents):
+                        parent_models = self.app_models.get(parent._meta.label, {})
+                        # update app_label and installed attribute of parent models
+                        for model in parent_models.itervalues():
+                            model._meta.app_label = app._meta.label
+                            model._meta.installed = True
+                        app._meta.models.update(parent_models)
+
+                # check if there is more than one app with the same
+                # db_prefix attribute
+                for app1 in self.loaded_apps:
+                    for app2 in self.loaded_apps:
+                        if (app1 != app2 and
+                                app1._meta.db_prefix == app2._meta.db_prefix):
+                            raise ImproperlyConfigured(
+                                'The apps "%s" and "%s" have the same db_prefix "%s"'
+                                % (app1, app2, app1._meta.db_prefix))
+                self.loaded = True
+                # send the post_apps_loaded signal
+                post_apps_loaded.send(sender=self, apps=self.loaded_apps)
+        finally:
+            self.write_lock.release()
+
+    def get_app_class(self, app_name):
+        """
+        Returns an app class for the given app name, which can be a
+        dotted path to an app class or a dotted app module path.
+        """
+        try:
+            app_path, app_attr = app_name.rsplit('.', 1)
+        except ValueError:
+            # First, return a new app class for the given module if
+            # it's one level module path that can't be rsplit (e.g. 'myapp')
+            return App.from_name(app_name)
+        try:
+            # Secondly, try to import the module directly,
+            # because it'll fail with a class path or a bad path
+            app_module = import_module(app_path)
+        except ImportError, e:
+            raise ImproperlyConfigured(
+                "Could not import app '%s': %s" % (app_path, e))
+        else:
+            # Thirdly, check if there is the submodule and fall back if yes
+            # If not look for the app class and do some type checks
+            if not module_has_submodule(app_module, app_attr):
+                try:
+                    app_class = getattr(app_module, app_attr)
+                except AttributeError:
+                    raise ImproperlyConfigured(
+                        "Could not find app '%s' in "
+                        "module '%s'" % (app_attr, app_path))
+                else:
+                    if not issubclass(app_class, App):
+                        raise ImproperlyConfigured(
+                            "App '%s' must be a subclass of "
+                            "'django.apps.App'" % app_name)
+                    return app_class
+        return App.from_name(app_name)
+
+    def load_app(self, app_name, app_kwargs=None, can_postpone=False):
+        """
+        Loads the app with the provided fully qualified name, and returns the
+        model module.
+
+        Keyword Arguments:
+            app_name: fully qualified name (e.g. 'django.contrib.auth')
+            can_postpone: If set to True and the import raises an ImportError
+                the loading will be postponed and tried again when all other
+                modules are loaded.
+        """
+        if app_kwargs is None:
+            app_kwargs = {}
+
+        self.handled.append(app_name)
+        self.nesting_level += 1
+
+        # check if an app instance with app_name already exists, if not
+        # then create one
+        app = self.find_app(app_name.split('.')[-1])
+        if not app:
+            app_class = self.get_app_class(app_name)
+            app = app_class(**app_kwargs)
+            self.loaded_apps.append(app)
+            # Send the signal that the app has been loaded
+            app_loaded.send(sender=app_class, app=app)
+
+        # import the app's models module and handle ImportErrors
+        try:
+            models = import_module(app._meta.models_path)
+        except ImportError:
+            self.nesting_level -= 1
+            # If the app doesn't have a models module, we can just ignore the
+            # ImportError and return no models for it.
+            if not module_has_submodule(app._meta.module, 'models'):
+                return None
+            # But if the app does have a models module, we need to figure out
+            # whether to suppress or propagate the error. If can_postpone is
+            # True then it may be that the package is still being imported by
+            # Python and the models module isn't available yet. So we add the
+            # app to the postponed list and we'll try it again after all the
+            # recursion has finished (in populate). If can_postpone is False
+            # then it's time to raise the ImportError.
+            else:
+                if can_postpone:
+                    self.postponed.append((app_name, app_kwargs))
+                    return None
+                else:
+                    raise
+
+        self.nesting_level -= 1
+        app._meta.models_module = models
+        return models
+
+    def find_app(self, app_label):
+        """
+        Returns the app instance that matches the given label.
+        """
+        for app in self.loaded_apps:
+            if app._meta.label == app_label:
+                return app
+
+    def find_app_by_models_module(self, models_module):
+        """
+        Returns the app instance that matches the models module
+        """
+        for app in self.loaded_apps:
+            if app._meta.models_module == models_module:
+                return app
+
+    def app_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 models modules.
+        """
+        self._populate()
+        return [app._meta.models_module for app in self.loaded_apps
+                if app._meta.models_module]
+
+    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()
+        app = self.find_app(app_label)
+        if app:
+            mod = app._meta.models_module
+            if mod is None:
+                if emptyOK:
+                    return None
+            else:
+                return mod
+        raise ImproperlyConfigured(
+                "App with label %s could not be found" % app_label)
+
+    def get_app_errors(self):
+        """
+        Returns the map of known problems with the INSTALLED_APPS.
+        """
+        self._populate()
+        errors = {}
+        for app in self.loaded_apps:
+            if app._meta.errors:
+                errors.update({app._meta.label: app._meta.errors})
+        return errors
+
+    def get_models(self, app_mod=None,
+                   include_auto_created=False, include_deferred=False,
+                   only_installed=True):
+        """
+        Given a module containing models, returns a list of the models.
+        Otherwise returns a list of all installed models.
+
+        By default, auto-created models (i.e., m2m models without an
+        explicit intermediate table) are not included. However, if you
+        specify include_auto_created=True, they will be.
+
+        By default, models created to satisfy deferred attribute
+        queries are *not* included in the list of models. However, if
+        you specify include_deferred, they will be.
+        """
+        cache_key = (app_mod, include_auto_created, include_deferred,
+                     only_installed)
+        try:
+            return self._get_models_cache[cache_key]
+        except KeyError:
+            pass
+        self._populate()
+        app_list = []
+        if app_mod and only_installed:
+            app_label = app_mod.__name__.split('.')[-2]
+            app = self.find_app(app_label)
+            if app:
+                app_list = [app._meta.models]
+        else:
+            if only_installed:
+                app_list = [app._meta.models for app in self.loaded_apps]
+            else:
+                app_list = self.app_models.itervalues()
+        model_list = []
+        for app in app_list:
+            model_list.extend(
+                model for model in app.values()
+                if ((not model._deferred or include_deferred) and
+                    (not model._meta.auto_created or include_auto_created))
+            )
+        self._get_models_cache[cache_key] = model_list
+        return model_list
+
+    def get_model(self, app_label, model_name,
+                  seed_cache=True, only_installed=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()
+        if only_installed:
+            app = self.find_app(app_label)
+            if not app:
+                return
+            return app._meta.models.get(model_name.lower())
+        return self.app_models.get(app_label, SortedDict()).get(model_name.lower())
+
+    def register_models(self, app_label, *models):
+        """
+        Register a set of models as belonging to an app.
+        """
+        app = self.find_app(app_label)
+        for model in models:
+            model_name = model._meta.object_name.lower()
+            model_dict = self.app_models.setdefault(app_label, SortedDict())
+            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
+            if app:
+                app._meta.models[model_name] = model
+            model_dict[model_name] = model
+        self._get_models_cache.clear()
diff --git a/django/apps/options.py b/django/apps/options.py
new file mode 100644
index 0000000..7a71552
--- /dev/null
+++ b/django/apps/options.py
@@ -0,0 +1,50 @@
+import re
+import os
+
+from django.utils.datastructures import SortedDict
+from django.utils.importlib import import_module
+from django.utils.text import get_verbose_name
+
+DEFAULT_NAMES = ('verbose_name', 'db_prefix', 'models_path')
+
+
+class AppOptions(object):
+    def __init__(self, name, meta):
+        self.name = name
+        self.meta = meta
+        self.errors = []
+        self.models = SortedDict()
+
+    def contribute_to_class(self, cls, name):
+        cls._meta = self
+        # get the name from the path e.g. "auth" for "django.contrib.auth"
+        self.label = self.name.split('.')[-1]
+        self.models_module = None
+        self.module = import_module(self.name)
+        self.path = os.path.dirname(self.module.__file__)
+        defaults = {
+            'db_prefix': self.label,
+            'models_path': '%s.models' % self.name,
+            'verbose_name': get_verbose_name(self.label),
+        }
+        # Next, apply any overridden values from 'class Meta'.
+        if self.meta:
+            meta_attrs = self.meta.__dict__.copy()
+            for name in self.meta.__dict__:
+                # Ignore any private attributes that Django doesn't care about.
+                if name.startswith('_'):
+                    del meta_attrs[name]
+            for attr_name in DEFAULT_NAMES:
+                if attr_name in meta_attrs:
+                    setattr(self, attr_name, meta_attrs.pop(attr_name))
+                elif hasattr(self.meta, attr_name):
+                    setattr(self, attr_name, getattr(self.meta, attr_name))
+            # Any leftover attributes must be invalid.
+            if meta_attrs != {}:
+                raise TypeError("'class Meta' got invalid attribute(s): %s"
+                                % ','.join(meta_attrs.keys()))
+        del self.meta
+
+        for k, v in defaults.iteritems():
+            if not hasattr(self, k):
+                setattr(self, k, v)
diff --git a/django/apps/signals.py b/django/apps/signals.py
new file mode 100644
index 0000000..befb77c
--- /dev/null
+++ b/django/apps/signals.py
@@ -0,0 +1,7 @@
+from django.dispatch import Signal
+
+# Sent when an app is loaded by the app cache
+app_loaded = Signal(providing_args=["app"])
+
+# Sent when the app cache loads the apps
+post_apps_loaded = Signal(providing_args=["apps"])
diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py
index 1084784..0507223 100644
--- a/django/contrib/admin/__init__.py
+++ b/django/contrib/admin/__init__.py
@@ -1,5 +1,6 @@
 # ACTION_CHECKBOX_NAME is unused, but should stay since its import from here
 # has been referenced in documentation.
+from django.apps import cache
 from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
 from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
 from django.contrib.admin.options import StackedInline, TabularInline
@@ -8,7 +9,6 @@ from django.contrib.admin.filters import (ListFilter, SimpleListFilter,
     FieldListFilter, BooleanFieldListFilter, RelatedFieldListFilter,
     ChoicesFieldListFilter, DateFieldListFilter, AllValuesFieldListFilter)
 
-
 def autodiscover():
     """
     Auto-discover INSTALLED_APPS admin.py modules and fail silently when
@@ -21,12 +21,11 @@ def autodiscover():
     from django.utils.importlib import import_module
     from django.utils.module_loading import module_has_submodule
 
-    for app in settings.INSTALLED_APPS:
-        mod = import_module(app)
+    for app in cache.loaded_apps:
         # Attempt to import the app's admin module.
         try:
             before_import_registry = copy.copy(site._registry)
-            import_module('%s.admin' % app)
+            import_module('%s.admin' % app._meta.name)
         except:
             # Reset the model registry to the state before the last import as
             # this import will have to reoccur on the next request and this
@@ -37,5 +36,5 @@ def autodiscover():
             # Decide whether to bubble up this error. If the app just
             # doesn't have an admin module, we can ignore the error
             # attempting to import it, otherwise we want it to bubble up.
-            if module_has_submodule(mod, 'admin'):
+            if module_has_submodule(app._meta.module, 'admin'):
                 raise
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index cf7ea83..6a0f5dd 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -1,5 +1,5 @@
 from functools import update_wrapper, partial
-from django import forms
+from django import apps, forms, template
 from django.forms.formsets import all_valid
 from django.forms.models import (modelform_factory, modelformset_factory,
     inlineformset_factory, BaseInlineFormSet)
@@ -968,7 +968,7 @@ class ModelAdmin(BaseModelAdmin):
             'media': mark_safe(media),
             'inline_admin_formsets': inline_admin_formsets,
             'errors': helpers.AdminErrorList(form, formsets),
-            'app_label': opts.app_label,
+            'app_label': apps.find_app(opts.app_label)._meta.verbose_name,
         }
         context.update(extra_context or {})
         return self.render_change_form(request, context, form_url=form_url, add=True)
@@ -1058,7 +1058,7 @@ class ModelAdmin(BaseModelAdmin):
             'media': mark_safe(media),
             'inline_admin_formsets': inline_admin_formsets,
             'errors': helpers.AdminErrorList(form, formsets),
-            'app_label': opts.app_label,
+            'app_label': apps.find_app(opts.app_label)._meta.verbose_name,
         }
         context.update(extra_context or {})
         return self.render_change_form(request, context, change=True, obj=obj)
@@ -1199,7 +1199,7 @@ class ModelAdmin(BaseModelAdmin):
             'cl': cl,
             'media': media,
             'has_add_permission': self.has_add_permission(request),
-            'app_label': app_label,
+            'app_label': apps.find_app(app_label)._meta.verbose_name,
             'action_form': action_form,
             'actions_on_top': self.actions_on_top,
             'actions_on_bottom': self.actions_on_bottom,
@@ -1263,7 +1263,7 @@ class ModelAdmin(BaseModelAdmin):
             "perms_lacking": perms_needed,
             "protected": protected,
             "opts": opts,
-            "app_label": app_label,
+            "app_label": apps.find_app(app_label)._meta.verbose_name,
         }
         context.update(extra_context or {})
 
@@ -1290,7 +1290,7 @@ class ModelAdmin(BaseModelAdmin):
             'action_list': action_list,
             'module_name': capfirst(force_unicode(opts.verbose_name_plural)),
             'object': obj,
-            'app_label': app_label,
+            'app_label': apps.find_app(app_label)._meta.verbose_name,
         }
         context.update(extra_context or {})
         return TemplateResponse(request, self.object_history_template or [
diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py
index 49c1e78..92b4478 100644
--- a/django/contrib/admin/sites.py
+++ b/django/contrib/admin/sites.py
@@ -1,5 +1,6 @@
 from functools import update_wrapper
-from django import http
+
+from django import apps, http
 from django.contrib.admin import ModelAdmin, actions
 from django.contrib.admin.forms import AdminAuthenticationForm
 from django.contrib.auth import REDIRECT_FIELD_NAME
@@ -348,7 +349,7 @@ class AdminSite(object):
                         app_dict[app_label]['models'].append(model_dict)
                     else:
                         app_dict[app_label] = {
-                            'name': app_label.title(),
+                            'name': apps.find_app(app_label)._meta.verbose_name,
                             'app_url': app_label + '/',
                             'has_module_perms': has_module_perms,
                             'models': [model_dict],
@@ -375,6 +376,7 @@ class AdminSite(object):
         user = request.user
         has_module_perms = user.has_module_perms(app_label)
         app_dict = {}
+        app = apps.find_app(app_label)
         for model, model_admin in self._registry.items():
             if app_label == model._meta.app_label:
                 if has_module_perms:
@@ -395,7 +397,7 @@ class AdminSite(object):
                             # something to display, add in the necessary meta
                             # information.
                             app_dict = {
-                                'name': app_label.title(),
+                                'name': app._meta.verbose_name,
                                 'app_url': '',
                                 'has_module_perms': has_module_perms,
                                 'models': [model_dict],
@@ -405,7 +407,7 @@ class AdminSite(object):
         # Sort the models alphabetically within each app.
         app_dict['models'].sort(key=lambda x: x['name'])
         context = {
-            'title': _('%s administration') % capfirst(app_label),
+            'title': _('%s administration') % app._meta.verbose_name,
             'app_list': [app_dict],
         }
         context.update(extra_context or {})
diff --git a/django/contrib/auth/app.py b/django/contrib/auth/app.py
new file mode 100644
index 0000000..159b713
--- /dev/null
+++ b/django/contrib/auth/app.py
@@ -0,0 +1,7 @@
+from django import apps
+from django.utils.translation import ugettext_lazy as _
+
+class AuthApp(apps.App):
+
+    class Meta:
+        verbose_name = _('auth')
diff --git a/django/contrib/contenttypes/management.py b/django/contrib/contenttypes/management.py
index 27d1275..a444714 100644
--- a/django/contrib/contenttypes/management.py
+++ b/django/contrib/contenttypes/management.py
@@ -1,5 +1,6 @@
+from django.apps import cache, get_apps, get_models
 from django.contrib.contenttypes.models import ContentType
-from django.db.models import get_apps, get_models, signals
+from django.db.models import signals
 from django.utils.encoding import smart_unicode
 
 def update_contenttypes(app, created_models, verbosity=2, **kwargs):
@@ -7,8 +8,9 @@ def update_contenttypes(app, created_models, verbosity=2, **kwargs):
     Creates content types for models in the given app, removing any model
     entries that no longer have a matching model class.
     """
+    app_cls = cache.find_app_by_models_module(app)
     ContentType.objects.clear_cache()
-    content_types = list(ContentType.objects.filter(app_label=app.__name__.split('.')[-2]))
+    content_types = list(ContentType.objects.filter(app_label=app_cls._meta.label))
     app_models = get_models(app)
     if not app_models:
         return
diff --git a/django/contrib/staticfiles/finders.py b/django/contrib/staticfiles/finders.py
index 45bf4a1..b23428b 100644
--- a/django/contrib/staticfiles/finders.py
+++ b/django/contrib/staticfiles/finders.py
@@ -1,4 +1,5 @@
 import os
+from django.apps import cache
 from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured
 from django.core.files.storage import default_storage, Storage, FileSystemStorage
@@ -113,14 +114,12 @@ class AppDirectoriesFinder(BaseFinder):
     """
     storage_class = AppStaticStorage
 
-    def __init__(self, apps=None, *args, **kwargs):
+    def __init__(self, *args, **kwargs):
         # The list of apps that are handled
         self.apps = []
         # Mapping of app module paths to storage instances
         self.storages = SortedDict()
-        if apps is None:
-            apps = settings.INSTALLED_APPS
-        for app in apps:
+        for app in cache.loaded_apps:
             app_storage = self.storage_class(app)
             if os.path.isdir(app_storage.location):
                 self.storages[app] = app_storage
diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py
index a804c56..ef04f20 100644
--- a/django/contrib/staticfiles/storage.py
+++ b/django/contrib/staticfiles/storage.py
@@ -194,9 +194,7 @@ class AppStaticStorage(FileSystemStorage):
         Returns a static file storage if available in the given app.
         """
         # app is the actual app module
-        mod = import_module(app)
-        mod_path = os.path.dirname(mod.__file__)
-        location = os.path.join(mod_path, self.source_dir)
+        location = os.path.join(app._meta.path, self.source_dir)
         super(AppStaticStorage, self).__init__(location, *args, **kwargs)
 
 
diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py
index 1345eaf..bf3790d 100644
--- a/django/core/management/__init__.py
+++ b/django/core/management/__init__.py
@@ -97,8 +97,8 @@ def get_commands():
 
         # Find the installed apps
         try:
-            from django.conf import settings
-            apps = settings.INSTALLED_APPS
+            from django.apps import cache
+            apps = cache.loaded_apps
         except (AttributeError, EnvironmentError, ImportError):
             apps = []
 
@@ -111,10 +111,10 @@ def get_commands():
             project_directory = None
 
         # Find and load the management module for each installed app.
-        for app_name in apps:
+        for app in apps:
             try:
-                path = find_management_module(app_name)
-                _commands.update(dict([(name, app_name)
+                path = find_management_module(app._meta.name)
+                _commands.update(dict([(name, app._meta.name)
                                        for name in find_commands(path)]))
             except ImportError:
                 pass # No management module - ignore this app
@@ -316,7 +316,7 @@ class ManagementUtility(object):
                 try:
                     from django.conf import settings
                     # Get the last part of the dotted path as the app name.
-                    options += [(a.split('.')[-1], 0) for a in settings.INSTALLED_APPS]
+                    options += [(a._meta.label, 0) for app in cache.loaded_apps]
                 except ImportError:
                     # Fail silently if DJANGO_SETTINGS_MODULE isn't set. The
                     # user will find out once they execute the command.
@@ -423,6 +423,12 @@ def setup_environ(settings_mod, original_settings_path=None):
     project_module = import_module(project_name)
     sys.path.pop()
 
+    # Initialize the appcache and look for errors
+    from django.apps import cache
+    for (app_name, error) in cache.get_app_errors().items():
+        sys.stderr.write("%s: %s" % (app_name, error))
+        sys.exit(1)
+
     return project_directory
 
 def execute_from_command_line(argv=None):
diff --git a/django/core/management/commands/flush.py b/django/core/management/commands/flush.py
index bcadb96..dcb2a1a 100644
--- a/django/core/management/commands/flush.py
+++ b/django/core/management/commands/flush.py
@@ -1,5 +1,6 @@
 from optparse import make_option
 
+from django.apps import cache
 from django.conf import settings
 from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS
 from django.core.management import call_command
@@ -29,9 +30,9 @@ class Command(NoArgsCommand):
 
         # Import the 'management' module within each installed app, to register
         # dispatcher events.
-        for app_name in settings.INSTALLED_APPS:
+        for app in cache.loaded_apps:
             try:
-                import_module('.management', app_name)
+                import_module('%s.management' % app._meta.name)
             except ImportError:
                 pass
 
diff --git a/django/core/management/commands/syncdb.py b/django/core/management/commands/syncdb.py
index 4383c34..13b8224 100644
--- a/django/core/management/commands/syncdb.py
+++ b/django/core/management/commands/syncdb.py
@@ -1,6 +1,7 @@
 from optparse import make_option
 import sys
 
+from django.apps import cache
 from django.conf import settings
 from django.core.management.base import NoArgsCommand
 from django.core.management.color import no_style
@@ -18,7 +19,7 @@ class Command(NoArgsCommand):
             default=DEFAULT_DB_ALIAS, help='Nominates a database to synchronize. '
                 'Defaults to the "default" database.'),
     )
-    help = "Create the database tables for all apps in INSTALLED_APPS whose tables haven't already been created."
+    help = "Create the database tables for all installed apps whose tables haven't already been created."
 
     def handle_noargs(self, **options):
 
@@ -34,9 +35,9 @@ class Command(NoArgsCommand):
 
         # Import the 'management' module within each installed app, to register
         # dispatcher events.
-        for app_name in settings.INSTALLED_APPS:
+        for app in cache.loaded_apps:
             try:
-                import_module('.management', app_name)
+                import_module('.management', app._meta.name)
             except ImportError, exc:
                 # This is slightly hackish. We want to ignore ImportErrors
                 # if the "management" module itself is missing -- but we don't
@@ -63,10 +64,11 @@ class Command(NoArgsCommand):
 
         # Build the manifest of apps and models that are to be synchronized
         all_models = [
-            (app.__name__.split('.')[-2],
-                [m for m in models.get_models(app, include_auto_created=True)
+            (app._meta.label,
+                [m for m in models.get_models(app._meta.models_module,
+                                              include_auto_created=True)
                 if router.allow_syncdb(db, m)])
-            for app in models.get_apps()
+            for app in cache.loaded_apps
         ]
         def model_installed(model):
             opts = model._meta
@@ -101,7 +103,6 @@ class Command(NoArgsCommand):
                     cursor.execute(statement)
                 tables.append(connection.introspection.table_name_converter(model._meta.db_table))
 
-
         transaction.commit_unless_managed(using=db)
 
         # Send the post_syncdb signal, so individual apps can do whatever they need
diff --git a/django/core/management/sql.py b/django/core/management/sql.py
index 298882f..1b170c2 100644
--- a/django/core/management/sql.py
+++ b/django/core/management/sql.py
@@ -1,10 +1,10 @@
 import os
 import re
 
+from django.apps import cache, get_models
 from django.conf import settings
 from django.core.management.base import CommandError
 from django.db import models
-from django.db.models import get_models
 
 def sql_create(app, style, connection):
     "Returns a list of the CREATE TABLE SQL statements for the given app."
@@ -182,7 +182,8 @@ def custom_sql_for_model(model, style, connection):
 def emit_post_sync_signal(created_models, verbosity, interactive, db):
     # Emit the post_sync signal for every application.
     for app in models.get_apps():
-        app_name = app.__name__.split('.')[-2]
+        app_cls = cache.find_app_by_models_module(app)
+        app_name = app_cls._meta.label
         if verbosity >= 2:
             print "Running post-sync handlers for application", app_name
         models.signals.post_syncdb.send(sender=app, app=app,
diff --git a/django/db/models/base.py b/django/db/models/base.py
index 71fd1f7..7891676 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -118,7 +118,7 @@ class ModelBase(type):
                 else:
                     base = parent
             if base is None:
-                    raise TypeError("Proxy model '%s' has no non-abstract model base class." % name)
+                raise TypeError("Proxy model '%s' has no non-abstract model base class." % name)
             if (new_class._meta.local_fields or
                     new_class._meta.local_many_to_many):
                 raise FieldError("Proxy model '%s' contains model fields." % name)
diff --git a/django/db/models/loading.py b/django/db/models/loading.py
index 03cdcc2..22c2c41 100644
--- a/django/db/models/loading.py
+++ b/django/db/models/loading.py
@@ -1,251 +1,10 @@
-"Utilities for loading models and the modules that contain them."
+import warnings
 
-from django.conf import settings
-from django.core.exceptions import ImproperlyConfigured
-from django.utils.datastructures import SortedDict
-from django.utils.importlib import import_module
-from django.utils.module_loading import module_has_submodule
+from django.apps import (App, AppCache, cache,
+    find_app, get_apps, get_app, get_app_errors, get_models, get_model,
+    register_models, load_app, app_cache_ready)
 
-import sys
-import os
-import threading
-
-__all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models',
-        'load_app', 'app_cache_ready')
-
-class AppCache(object):
-    """
-    A cache that stores installed applications and their models. Used to
-    provide reverse-relations and for app introspection (e.g. admin).
-    """
-    # 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 = SortedDict(),
-
-        # Mapping of installed app_labels to model modules for that app.
-        app_labels = {},
-
-        # Mapping of app_labels to a dictionary of model names to model code.
-        # May contain apps that are not installed.
-        app_models = SortedDict(),
-
-        # Mapping of app_labels to errors raised when trying to import the app.
-        app_errors = {},
-
-        # -- Everything below here is only used when populating the cache --
-        loaded = False,
-        handled = {},
-        postponed = [],
-        nesting_level = 0,
-        write_lock = threading.RLock(),
-        _get_models_cache = {},
-    )
-
-    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
-                self.load_app(app_name, True)
-            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 _label_for(self, app_mod):
-        """
-        Return app_label for given models module.
-
-        """
-        return app_mod.__name__.split('.')[-2]
-
-    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
-        app_module = import_module(app_name)
-        try:
-            models = import_module('.models', app_name)
-        except ImportError:
-            self.nesting_level -= 1
-            # If the app doesn't have a models module, we can just ignore the
-            # ImportError and return no models for it.
-            if not module_has_submodule(app_module, 'models'):
-                return None
-            # But if the app does have a models module, we need to figure out
-            # whether to suppress or propagate the error. If can_postpone is
-            # True then it may be that the package is still being imported by
-            # Python and the models module isn't available yet. So we add the
-            # app to the postponed list and we'll try it again after all the
-            # recursion has finished (in populate). If can_postpone is False
-            # then it's time to raise the ImportError.
-            else:
-                if can_postpone:
-                    self.postponed.append(app_name)
-                    return None
-                else:
-                    raise
-
-        self.nesting_level -= 1
-        if models not in self.app_store:
-            self.app_store[models] = len(self.app_store)
-            self.app_labels[self._label_for(models)] = models
-        return models
-
-    def app_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,
-                   include_auto_created=False, include_deferred=False,
-                   only_installed=True):
-        """
-        Given a module containing models, returns a list of the models.
-        Otherwise returns a list of all installed models.
-
-        By default, auto-created models (i.e., m2m models without an
-        explicit intermediate table) are not included. However, if you
-        specify include_auto_created=True, they will be.
-
-        By default, models created to satisfy deferred attribute
-        queries are *not* included in the list of models. However, if
-        you specify include_deferred, they will be.
-        """
-        cache_key = (app_mod, include_auto_created, include_deferred, only_installed)
-        try:
-            return self._get_models_cache[cache_key]
-        except KeyError:
-            pass
-        self._populate()
-        if app_mod:
-            if app_mod in self.app_store:
-                app_list = [self.app_models.get(self._label_for(app_mod),
-                                                SortedDict())]
-            else:
-                app_list = []
-        else:
-            if only_installed:
-                app_list = [self.app_models.get(app_label, SortedDict())
-                            for app_label in self.app_labels.iterkeys()]
-            else:
-                app_list = self.app_models.itervalues()
-        model_list = []
-        for app in app_list:
-            model_list.extend(
-                model for model in app.values()
-                if ((not model._deferred or include_deferred) and
-                    (not model._meta.auto_created or include_auto_created))
-            )
-        self._get_models_cache[cache_key] = model_list
-        return model_list
-
-    def get_model(self, app_label, model_name,
-                  seed_cache=True, only_installed=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()
-        if only_installed and app_label not in self.app_labels:
-            return None
-        return self.app_models.get(app_label, SortedDict()).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, SortedDict())
-            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
-        self._get_models_cache.clear()
-
-cache = AppCache()
-
-# 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
-app_cache_ready = cache.app_cache_ready
+warnings.warn(
+    'The utilities in django.db.models.loading have been moved to '
+    'django.apps. Please update your code accordingly.',
+    PendingDeprecationWarning)
diff --git a/django/db/models/options.py b/django/db/models/options.py
index 6f0f406..5d19fe9 100644
--- a/django/db/models/options.py
+++ b/django/db/models/options.py
@@ -1,6 +1,7 @@
 import re
 from bisect import bisect
 
+from django.apps import cache
 from django.conf import settings
 from django.db.models.related import RelatedObject
 from django.db.models.fields.related import ManyToManyRel
@@ -10,9 +11,8 @@ from django.db.models.loading import get_models, app_cache_ready
 from django.utils.translation import activate, deactivate_all, get_language, string_concat
 from django.utils.encoding import force_unicode, smart_str
 from django.utils.datastructures import SortedDict
+from django.utils.text import get_verbose_name
 
-# Calculate the verbose_name by converting from InitialCaps to "lowercase with spaces".
-get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).lower().strip()
 
 DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering',
                  'unique_together', 'permissions', 'get_latest_by',
@@ -59,7 +59,8 @@ class Options(object):
         from django.db.backends.util import truncate_name
 
         cls._meta = self
-        self.installed = re.sub('\.models$', '', cls.__module__) in settings.INSTALLED_APPS
+        # The AppCache sets this attribute to True for apps that are installed
+        self.installed = False
         # First, construct the default values for these options.
         self.object_name = cls.__name__
         self.module_name = self.object_name.lower()
@@ -100,10 +101,14 @@ class Options(object):
             self.verbose_name_plural = string_concat(self.verbose_name, 's')
         del self.meta
 
-        # If the db_table wasn't provided, use the app_label + module_name.
+        # If the db_table wasn't provided, use the db_prefix + module_name.
+        # Or use the app label when no app instance was found, which happens
+        # when the app cache is not initialized but the model is imported
         if not self.db_table:
-            self.db_table = "%s_%s" % (self.app_label, self.module_name)
-            self.db_table = truncate_name(self.db_table, connection.ops.max_name_length())
+            app = cache.find_app(self.app_label)
+            prefix = app and app._meta.db_prefix or self.app_label
+            self.db_table = truncate_name("%s_%s" % (prefix, self.module_name),
+                                          connection.ops.max_name_length())
 
     def _prepare(self, model):
         if self.order_with_respect_to:
diff --git a/django/template/base.py b/django/template/base.py
index 9a7f55f..0acda61 100644
--- a/django/template/base.py
+++ b/django/template/base.py
@@ -2,6 +2,7 @@ import re
 from functools import partial
 from inspect import getargspec
 
+from django.apps import cache
 from django.conf import settings
 from django.template.context import Context, RequestContext, ContextPopException
 from django.utils.importlib import import_module
@@ -1046,7 +1047,7 @@ def get_templatetags_modules():
     if not templatetags_modules:
         _templatetags_modules = []
         # Populate list once per thread.
-        for app_module in ['django'] + list(settings.INSTALLED_APPS):
+        for app_module in ['django'] + [app._meta.name for app in cache.loaded_apps]:
             try:
                 templatetag_module = '%s.templatetags' % app_module
                 import_module(templatetag_module)
diff --git a/django/template/loaders/app_directories.py b/django/template/loaders/app_directories.py
index b0560b4..b401e6c 100644
--- a/django/template/loaders/app_directories.py
+++ b/django/template/loaders/app_directories.py
@@ -6,6 +6,7 @@ packages.
 import os
 import sys
 
+from django.apps import cache
 from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured
 from django.template.base import TemplateDoesNotExist
@@ -16,12 +17,8 @@ from django.utils.importlib import import_module
 # At compile time, cache the directories to search.
 fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
 app_template_dirs = []
-for app in settings.INSTALLED_APPS:
-    try:
-        mod = import_module(app)
-    except ImportError, e:
-        raise ImproperlyConfigured('ImportError %s: %s' % (app, e.args[0]))
-    template_dir = os.path.join(os.path.dirname(mod.__file__), 'templates')
+for app in cache.loaded_apps:
+    template_dir = os.path.join(app._meta.path, 'templates')
     if os.path.isdir(template_dir):
         app_template_dirs.append(template_dir.decode(fs_encoding))
 
diff --git a/django/template/loaders/eggs.py b/django/template/loaders/eggs.py
index 42f87a4..a5ef838 100644
--- a/django/template/loaders/eggs.py
+++ b/django/template/loaders/eggs.py
@@ -1,4 +1,5 @@
 # Wrapper for loading templates from eggs via pkg_resources.resource_string.
+from django.apps import cache
 
 try:
     from pkg_resources import resource_string
@@ -20,10 +21,10 @@ class Loader(BaseLoader):
         """
         if resource_string is not None:
             pkg_name = 'templates/' + template_name
-            for app in settings.INSTALLED_APPS:
+            for app in cache.loaded_apps:
                 try:
-                    return (resource_string(app, pkg_name).decode(settings.FILE_CHARSET), 'egg:%s:%s' % (app, pkg_name))
-                except:
+                    return (resource_string(app._meta.name, pkg_name).decode(settings.FILE_CHARSET), 'egg:%s:%s' % (app._meta.name, pkg_name))
+                except Exception, e:
                     pass
         raise TemplateDoesNotExist(template_name)
 
diff --git a/django/utils/module_loading.py b/django/utils/module_loading.py
index 26a7e51..54c04e9 100644
--- a/django/utils/module_loading.py
+++ b/django/utils/module_loading.py
@@ -14,6 +14,9 @@ def module_has_submodule(package, module_name):
     for finder in sys.meta_path:
         if finder.find_module(name, package.__path__):
             return True
+    # If this isn't really a package return False right away
+    if not hasattr(package, '__path__'):
+        return False
     for entry in package.__path__:  # No __path__, then not a package.
         try:
             # Try the cached finder.
diff --git a/django/utils/text.py b/django/utils/text.py
index 14555dd..2afb2b0 100644
--- a/django/utils/text.py
+++ b/django/utils/text.py
@@ -362,9 +362,9 @@ def unescape_entities(text):
 unescape_entities = allow_lazy(unescape_entities, unicode)
 
 def unescape_string_literal(s):
-    r"""
-    Convert quoted string literals to unquoted strings with escaped quotes and
-    backslashes unquoted::
+    """
+    Converts quoted string literals to unquoted strings with escaped quotes
+    and backslashes unquoted::
 
         >>> unescape_string_literal('"abc"')
         'abc'
@@ -380,3 +380,11 @@ def unescape_string_literal(s):
     quote = s[0]
     return s[1:-1].replace(r'\%s' % quote, quote).replace(r'\\', '\\')
 unescape_string_literal = allow_lazy(unescape_string_literal)
+
+def get_verbose_name(class_name):
+    """
+    Calculates the verbose_name by converting from
+    InitialCaps to "lowercase with spaces".
+    """
+    new = re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name)
+    return new.lower().strip()
diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py
index 71765e7..5eefd70 100644
--- a/django/utils/translation/trans_real.py
+++ b/django/utils/translation/trans_real.py
@@ -12,6 +12,7 @@ try:
 except ImportError:
     from StringIO import StringIO
 
+from django.apps import cache
 from django.utils.importlib import import_module
 from django.utils.safestring import mark_safe, SafeData
 
@@ -156,9 +157,8 @@ def translation(language):
                     res.merge(t)
             return res
 
-        for appname in reversed(settings.INSTALLED_APPS):
-            app = import_module(appname)
-            apppath = os.path.join(os.path.dirname(app.__file__), 'locale')
+        for app in reversed(cache.loaded_apps):
+            apppath = os.path.join(app._meta.path, 'locale')
 
             if os.path.isdir(apppath):
                 res = _merge(apppath)
diff --git a/django/views/i18n.py b/django/views/i18n.py
index 140dc54..055323e 100644
--- a/django/views/i18n.py
+++ b/django/views/i18n.py
@@ -2,6 +2,7 @@ import os
 import gettext as gettext_module
 
 from django import http
+from django.apps import cache
 from django.conf import settings
 from django.utils import importlib
 from django.utils.translation import check_for_language, activate, to_locale, get_language
@@ -186,7 +187,8 @@ def javascript_catalog(request, domain='djangojs', packages=None):
         packages = ['django.conf']
     if isinstance(packages, basestring):
         packages = packages.split('+')
-    packages = [p for p in packages if p == 'django.conf' or p in settings.INSTALLED_APPS]
+    apps = [app._meta.name for app in cache.loaded_apps]
+    packages = [p for p in packages if p == 'django.conf' or p in apps]
     default_locale = to_locale(settings.LANGUAGE_CODE)
     locale = to_locale(get_language())
     t = {}
diff --git a/tests/appcachetests/anothermodel_app/__init__.py b/tests/appcachetests/anothermodel_app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/appcachetests/anothermodel_app/app.py b/tests/appcachetests/anothermodel_app/app.py
new file mode 100644
index 0000000..7433592
--- /dev/null
+++ b/tests/appcachetests/anothermodel_app/app.py
@@ -0,0 +1,11 @@
+from django import apps
+
+class MyApp(apps.App):
+
+    class Meta:
+        models_path = 'model_app.othermodels'
+
+class MyOtherApp(MyApp):
+
+    class Meta:
+        db_prefix = 'nomodel_app'
diff --git a/tests/appcachetests/anothermodel_app/models.py b/tests/appcachetests/anothermodel_app/models.py
new file mode 100644
index 0000000..cd849d1
--- /dev/null
+++ b/tests/appcachetests/anothermodel_app/models.py
@@ -0,0 +1,12 @@
+from django.db import models
+
+class Job(models.Model):
+    name = models.CharField(max_length=30)
+
+class Person(models.Model):
+    first_name = models.CharField(max_length=30)
+    last_name = models.CharField(max_length=30)
+    jobs = models.ManyToManyField(Job)
+
+class Contact(models.Model):
+    person = models.ForeignKey(Person)
diff --git a/tests/appcachetests/anothermodel_app/othermodels.py b/tests/appcachetests/anothermodel_app/othermodels.py
new file mode 100644
index 0000000..ffb04e8
--- /dev/null
+++ b/tests/appcachetests/anothermodel_app/othermodels.py
@@ -0,0 +1,5 @@
+from django.db import models
+
+class Person(models.Model):
+    first_name = models.CharField(max_length=30)
+    last_name = models.CharField(max_length=30)
diff --git a/tests/appcachetests/eggs/brokenapp.egg b/tests/appcachetests/eggs/brokenapp.egg
new file mode 100755
index 0000000..8aca671
Binary files /dev/null and b/tests/appcachetests/eggs/brokenapp.egg differ
diff --git a/tests/appcachetests/eggs/modelapp.egg b/tests/appcachetests/eggs/modelapp.egg
new file mode 100755
index 0000000..c2370b5
Binary files /dev/null and b/tests/appcachetests/eggs/modelapp.egg differ
diff --git a/tests/appcachetests/eggs/nomodelapp.egg b/tests/appcachetests/eggs/nomodelapp.egg
new file mode 100755
index 0000000..5b8d217
Binary files /dev/null and b/tests/appcachetests/eggs/nomodelapp.egg differ
diff --git a/tests/appcachetests/eggs/omelet.egg b/tests/appcachetests/eggs/omelet.egg
new file mode 100755
index 0000000..bd1c687
Binary files /dev/null and b/tests/appcachetests/eggs/omelet.egg differ
diff --git a/tests/appcachetests/model_app/__init__.py b/tests/appcachetests/model_app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/appcachetests/model_app/app.py b/tests/appcachetests/model_app/app.py
new file mode 100644
index 0000000..3a3e307
--- /dev/null
+++ b/tests/appcachetests/model_app/app.py
@@ -0,0 +1,34 @@
+from django import apps
+
+class MyApp(apps.App):
+
+    class Meta:
+        models_path = 'model_app.othermodels'
+
+class MyOtherApp(MyApp):
+
+    class Meta(MyApp.Meta):
+        db_prefix = 'nomodel_app'
+
+
+class MySecondApp(MyOtherApp):
+
+    class Meta(MyOtherApp.Meta):
+        models_path = 'model_app.models'
+
+
+class YetAnotherApp(apps.App):
+
+    class Meta:
+        models_path = 'model_app.yetanother'
+
+
+class MyThirdApp(YetAnotherApp, MySecondApp):
+
+    class Meta(YetAnotherApp.Meta, MySecondApp.Meta):
+        pass
+
+
+class MyOverrideApp(MyOtherApp):
+
+    pass
diff --git a/tests/appcachetests/model_app/models.py b/tests/appcachetests/model_app/models.py
new file mode 100644
index 0000000..ffb04e8
--- /dev/null
+++ b/tests/appcachetests/model_app/models.py
@@ -0,0 +1,5 @@
+from django.db import models
+
+class Person(models.Model):
+    first_name = models.CharField(max_length=30)
+    last_name = models.CharField(max_length=30)
diff --git a/tests/appcachetests/model_app/othermodels.py b/tests/appcachetests/model_app/othermodels.py
new file mode 100644
index 0000000..ffb04e8
--- /dev/null
+++ b/tests/appcachetests/model_app/othermodels.py
@@ -0,0 +1,5 @@
+from django.db import models
+
+class Person(models.Model):
+    first_name = models.CharField(max_length=30)
+    last_name = models.CharField(max_length=30)
diff --git a/tests/appcachetests/model_app/yetanother.py b/tests/appcachetests/model_app/yetanother.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/appcachetests/nomodel_app/__init__.py b/tests/appcachetests/nomodel_app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/appcachetests/nomodel_app/app.py b/tests/appcachetests/nomodel_app/app.py
new file mode 100644
index 0000000..194278e
--- /dev/null
+++ b/tests/appcachetests/nomodel_app/app.py
@@ -0,0 +1,7 @@
+from django import apps
+
+class MyApp(apps.App):
+    pass
+
+class ObjectApp(object):
+    pass
\ No newline at end of file
diff --git a/tests/appcachetests/same_label/__init__.py b/tests/appcachetests/same_label/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/appcachetests/same_label/model_app/__init__.py b/tests/appcachetests/same_label/model_app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/appcachetests/same_label/model_app/models.py b/tests/appcachetests/same_label/model_app/models.py
new file mode 100644
index 0000000..ffb04e8
--- /dev/null
+++ b/tests/appcachetests/same_label/model_app/models.py
@@ -0,0 +1,5 @@
+from django.db import models
+
+class Person(models.Model):
+    first_name = models.CharField(max_length=30)
+    last_name = models.CharField(max_length=30)
diff --git a/tests/appcachetests/same_label/nomodel_app/__init__.py b/tests/appcachetests/same_label/nomodel_app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/appcachetests/same_label/nomodel_app/app.py b/tests/appcachetests/same_label/nomodel_app/app.py
new file mode 100644
index 0000000..86993e0
--- /dev/null
+++ b/tests/appcachetests/same_label/nomodel_app/app.py
@@ -0,0 +1,4 @@
+from django import apps
+
+class MyApp(apps.App):
+    pass
diff --git a/tests/appcachetests/tests.py b/tests/appcachetests/tests.py
new file mode 100755
index 0000000..113efe0
--- /dev/null
+++ b/tests/appcachetests/tests.py
@@ -0,0 +1,697 @@
+#!/usr/bin/env python
+import os
+import sys
+import unittest
+import threading
+
+from django.apps import cache
+from django.apps.cache import _initialize
+from django.apps.signals import app_loaded, post_apps_loaded
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.datastructures import SortedDict
+
+# remove when tests are integrated into the django testsuite
+settings.configure()
+
+
+class AppCacheTestCase(unittest.TestCase):
+    """
+    TestCase that resets the AppCache after each test.
+    """
+    def setUp(self):
+        self.old_installed_apps = settings.INSTALLED_APPS
+        settings.INSTALLED_APPS = ()
+        settings.DATABASES = {
+            'default': {
+                'ENGINE': 'django.db.backends.sqlite3',
+                'NAME': ':memory:'
+            }
+        }
+
+    def tearDown(self):
+        settings.INSTALLED_APPS = self.old_installed_apps
+        cache._reset()
+
+class ReloadTests(AppCacheTestCase):
+    """
+    Tests for the _reload function
+    """
+
+    def test_reload(self):
+        """
+        Test reloading the cache
+        """
+        settings.INSTALLED_APPS = ('model_app',)
+        cache._populate()
+        self.assertEquals(len(cache.loaded_apps), 1)
+        self.assertEquals(cache.loaded_apps[0]._meta.name, 'model_app')
+        settings.INSTALLED_APPS = ('anothermodel_app', 'model_app')
+        cache._reload()
+        self.assertEquals(len(cache.loaded_apps), 2)
+        self.assertEquals(cache.loaded_apps[0]._meta.name, 'anothermodel_app')
+
+    def test_reload_register_models(self):
+        """
+        Test that models are registered with the cache again after it
+        was reloaded
+        """
+        settings.INSTALLED_APPS = ('model_app',)
+        cache._populate()
+        self.assertTrue('model_app' in cache.app_models)
+        cache._reload()
+        self.assertTrue('model_app' in cache.app_models)
+
+
+class AppCacheReadyTests(AppCacheTestCase):
+    """
+    Tests for the app_cache_ready function that indicates if the cache
+    is fully populated.
+    """
+
+    def test_not_initialized(self):
+        """
+        Should return False if the AppCache hasn't been initialized
+        """
+        self.assertFalse(cache.app_cache_ready())
+
+    def test_load_app(self):
+        """
+        Should return False after executing the load_app function
+        """
+        cache.load_app('nomodel_app')
+        self.assertFalse(cache.app_cache_ready())
+        cache.load_app('nomodel_app', can_postpone=True)
+        self.assertFalse(cache.app_cache_ready())
+
+
+class GetAppClassTests(AppCacheTestCase):
+    """Tests for the get_app_class function"""
+
+    def test_app_class(self):
+        """
+        Tests that the full path app class is returned
+        """
+        settings.INSTALLED_APPS = ('model_app.app.MyApp',)
+        from model_app.app import MyApp
+        app_class = cache.get_app_class(settings.INSTALLED_APPS[0])
+        self.assertEquals(app_class, MyApp)
+
+    def test_one_level_module(self):
+        """
+        Tests that a new app class is generated for an one level app module
+        """
+        settings.INSTALLED_APPS = ('model_app',)
+        app_class = cache.get_app_class(settings.INSTALLED_APPS[0])
+        self.assertEquals(app_class.__name__, 'ModelApp')
+
+    def test_multi_level_module(self):
+        """
+        Tests that a new app class is generated for a multiple level app module
+        """
+        settings.INSTALLED_APPS = ('django.contrib.admin',)
+        app_class = cache.get_app_class(settings.INSTALLED_APPS[0])
+        self.assertEquals(app_class.__name__, 'Admin')
+
+    def test_defunct_module(self):
+        """
+        Tests that a wrong module raises an ImproperlyConfigured exception
+        """
+        settings.INSTALLED_APPS = ('lalalala.admin',)
+        self.assertRaises(ImproperlyConfigured, cache.get_app_class,
+                          settings.INSTALLED_APPS[0])
+
+    def test_missing_attribute(self):
+        """
+        Tests that a missing attribute raises an ImproperlyConfigured exception
+        """
+        settings.INSTALLED_APPS = ('nomodel_app.app.NotThereApp',)
+        self.assertRaises(ImproperlyConfigured, cache.get_app_class,
+                          settings.INSTALLED_APPS[0])
+
+    def test_incorrect_subclass(self):
+        """
+        Tests that a class not subclassing django.apps.App raises an
+        ImproperlyConfigured exception
+        """
+        settings.INSTALLED_APPS = ('nomodel_app.app.ObjectApp',)
+        self.assertRaises(ImproperlyConfigured, cache.get_app_class,
+                          settings.INSTALLED_APPS[0])
+
+
+class GetAppsTests(AppCacheTestCase):
+    """Tests for the get_apps function"""
+
+    def test_app_classes(self):
+        """
+        Test that the correct models modules are returned for app classes
+        installed via the INSTALLED_APPS setting
+        """
+        settings.INSTALLED_APPS = ('model_app.app.MyApp',)
+        apps = cache.get_apps()
+        self.assertTrue(cache.app_cache_ready())
+        self.assertEquals(apps[0].__name__, 'model_app.othermodels')
+
+    def test_installed_apps(self):
+        """
+        Test that the correct models modules are returned for apps installed
+        via the INSTALLED_APPS setting
+        """
+        settings.INSTALLED_APPS = ('model_app',)
+        apps = cache.get_apps()
+        self.assertTrue(cache.app_cache_ready())
+        self.assertEquals(apps[0].__name__, 'model_app.models')
+
+    def test_same_app_in_both_settings(self):
+        """
+        Test that if an App is listed multiple times in INSTALLED_APPS
+        only one of them is loaded
+        """
+        settings.INSTALLED_APPS = ('model_app.app.MyApp', 'model_app')
+        apps = cache.get_apps()
+        self.assertEquals(len(apps), 1)
+        self.assertEquals(apps[0].__name__, 'model_app.othermodels')
+
+    def test_empty_models(self):
+        """
+        Test that modules that don't contain models are not returned
+        """
+        settings.INSTALLED_APPS = ('nomodel_app',)
+        self.assertEqual(cache.get_apps(), [])
+        self.assertTrue(cache.app_cache_ready())
+
+    def test_db_prefix_exception(self):
+        """
+        Test that an exception is raised if two app instances
+        have the same db_prefix attribute
+        """
+        settings.INSTALLED_APPS = ('nomodel_app.app.MyApp',
+                                   'model_app.app.MyOtherApp')
+        self.assertRaises(ImproperlyConfigured, cache.get_apps)
+
+
+class GetAppTests(AppCacheTestCase):
+    """Tests for the get_app function"""
+
+    def test_installed_apps(self):
+        """
+        Test that the correct module is returned when the app was installed
+        via the INSTALLED_APPS setting
+        """
+        settings.INSTALLED_APPS = ('model_app',)
+        mod = cache.get_app('model_app')
+        self.assertTrue(cache.app_cache_ready())
+        self.assertEquals(mod.__name__, 'model_app.models')
+
+    def test_not_found_exception(self):
+        """
+        Test that an ImproperlyConfigured exception is raised if an app
+        could not be found
+        """
+        self.assertRaises(ImproperlyConfigured, cache.get_app,
+                          'notarealapp')
+        self.assertTrue(cache.app_cache_ready())
+
+    def test_emptyOK(self):
+        """
+        Test that None is returned if emptyOK is True and the module
+        has no models
+        """
+        settings.INSTALLED_APPS = ('nomodel_app',)
+        module = cache.get_app('nomodel_app', emptyOK=True)
+        self.assertTrue(cache.app_cache_ready())
+        self.failUnless(module is None)
+
+    def test_exception_if_no_models(self):
+        """
+        Test that an ImproperlyConfigured exception is raised if the app
+        has no modules and the emptyOK arg is False
+        """
+        settings.INSTALLED_APPS = ('nomodel_app',)
+        self.assertRaises(ImproperlyConfigured, cache.get_app,
+                          'nomodel_app')
+        self.assertTrue(cache.app_cache_ready())
+
+
+class GetAppErrorsTests(AppCacheTestCase):
+    """Tests for the get_app_errors function"""
+
+    def test_get_app_errors(self):
+        """Test that the function returns an empty dict"""
+        self.assertEqual(cache.get_app_errors(), {})
+        self.assertTrue(cache.app_cache_ready())
+
+
+class GetModelsTests(AppCacheTestCase):
+    """Tests for the get_models function"""
+
+    def test_installed(self):
+        """
+        Test that only models from apps are returned that are listed in
+        the INSTALLED_APPS setting
+        """
+        from anothermodel_app.models import Person
+        from model_app.models import Person
+        settings.INSTALLED_APPS = ('model_app',)
+        models = cache.get_models()
+        self.assertTrue(cache.app_cache_ready())
+        self.assertEqual(models, [Person])
+
+    def test_not_only_installed(self):
+        """
+        Test that not only installed models are returned
+        """
+        from anothermodel_app.models import Job, Person, Contact
+        from model_app.models import Person as p2
+        settings.INSTALLED_APPS = ('model_app',)
+        models = cache.get_models(only_installed=False)
+        self.assertTrue(cache.app_cache_ready())
+        self.assertEqual(models, [Job, Person, Contact, p2])
+
+    def test_app_mod(self):
+        """
+        Test that the correct models are returned if an models module is
+        passed and the app is listed in INSTALLED_APPS
+        """
+        from model_app import models
+        from model_app.models import Person
+        settings.INSTALLED_APPS = ('model_app', 'anothermodel_app')
+        models = cache.get_models(app_mod=models)
+        self.assertTrue(cache.app_cache_ready())
+        self.assertEqual(models, [Person])
+
+    def test_app_mod_not_installed(self):
+        """
+        Test that no models are returned when a models module is
+        passed and the app is _not_ listed in INSTALLED_APPS
+        """
+        from model_app import models
+        from model_app.models import Person
+        models = cache.get_models(app_mod=models)
+        self.assertEqual(models, [])
+
+    def test_include_auto_created(self):
+        """
+        Test that auto created models are included
+        """
+        settings.INSTALLED_APPS = ('anothermodel_app',)
+        models = cache.get_models(include_auto_created=True)
+        self.assertTrue(cache.app_cache_ready())
+        from anothermodel_app.models import Job, Person
+        self.assertEqual(models[0], Job)
+        self.assertEqual(models[1].__name__, 'Person_jobs')
+        self.assertEqual(models[2], Person)
+
+    def test_related_objects_cache(self):
+        """
+        Test that the related objects cache is filled correctly
+        """
+        from anothermodel_app.models import Contact
+        self.assertEqual(Contact._meta.get_all_field_names(),
+                         ['id', 'person'])
+
+    def test_related_many_to_many_cache(self):
+        """
+        Test that the related m2m cache is filled correctly
+        """
+        from anothermodel_app.models import Job
+        self.assertEqual(Job._meta.get_all_field_names(),
+                         ['id', 'name', 'person'])
+
+
+class GetModelTests(AppCacheTestCase):
+    """Tests for the get_model function"""
+
+    def test_seeded_only_installed_valid(self):
+        """
+        Test that the correct model is returned if the cache is seeded
+        and only models from apps listed in INSTALLED_APPS should be returned
+        """
+        settings.INSTALLED_APPS = ('model_app',)
+        model = cache.get_model('model_app', 'Person')
+        self.assertEqual(model.__name__, 'Person')
+        self.assertTrue(cache.app_cache_ready())
+
+    def test_seeded_only_installed_invalid(self):
+        """
+        Test that None is returned if the cache is seeded but the model
+        was not registered with the cache
+        """
+        model = cache.get_model('model_app', 'Person')
+        self.assertEqual(model, None)
+        self.assertTrue(cache.app_cache_ready())
+
+    def test_unseeded_only_installed_invalid(self):
+        """
+        Test that None is returned if the cache is unseeded and the model
+        was not registered with the cache
+        """
+        model = cache.get_model('model_app', 'Person', seed_cache=False)
+        self.assertEqual(model, None)
+        self.assertFalse(cache.app_cache_ready())
+
+    def test_seeded_all_models_valid(self):
+        """
+        Test that the correct model is returned if the cache is seeded and
+        all models (including unbound) should be returned
+        """
+        cache._populate()
+        from model_app.models import Person
+        model = cache.get_model('model_app', 'Person', only_installed=False)
+        self.assertEquals(model, Person)
+
+    def test_seeded_all_models_invalid(self):
+        """
+        Test that None is returned if the cache is seeded and all models
+        should be returned, but the model wasnt registered with the cache
+        """
+        cache._populate()
+        model = cache.get_model('model_app', 'Person', only_installed=False)
+        self.assertEquals(model, None)
+
+    def test_unseeded_all_models_valid(self):
+        """
+        Test that the correct model is returned if the cache is unseeded and
+        all models should be returned
+        """
+        from model_app.models import Person
+        model = cache.get_model('model_app', 'Person', seed_cache=False, only_installed=False)
+        self.assertEquals(model, Person)
+
+    def test_unseeded_all_models_invalid(self):
+        """
+        Test that None is returned if the cache is unseeded, all models should
+        be returned but the model wasn't registered with the cache
+        """
+        model = cache.get_model('model_app', 'Person', seed_cache=False, only_installed=False)
+        self.assertEquals(model, None)
+
+class LoadAppTests(AppCacheTestCase):
+    """Tests for the load_app function"""
+
+    def test_with_models(self):
+        """
+        Test that an app instance is created and the models
+        module is returned
+        """
+        mod = cache.load_app('model_app')
+        app = cache.loaded_apps[0]
+        self.assertEqual(len(cache.loaded_apps), 1)
+        self.assertEqual(app._meta.name, 'model_app')
+        self.assertEqual(app._meta.models_module.__name__, 'model_app.models')
+        self.assertEqual(mod.__name__, 'model_app.models')
+
+    def test_with_inheritance(self):
+        from model_app.app import MyApp
+        mod = cache.load_app('model_app.app.MyOtherApp')
+        app = cache.loaded_apps[0]
+        self.assertEqual(app._meta.name, 'model_app')
+        self.assertEqual(app._meta.models_module.__name__, 'model_app.othermodels')
+        self.assertEqual(mod.__name__, 'model_app.othermodels')
+        self.assertEqual(app.__class__.__bases__, (MyApp,))
+        self.assertEqual(app._meta.models_path, 'model_app.othermodels')
+        self.assertEqual(app._meta.db_prefix, 'nomodel_app')
+        self.assertEqual(app._meta.verbose_name, 'model_app')
+
+    def test_with_multiple_inheritance(self):
+        from model_app.app import MyOtherApp
+        from django.apps import App
+        mod = cache.load_app('model_app.app.MySecondApp')
+        app = cache.loaded_apps[0]
+        self.assertEqual(app._meta.name, 'model_app')
+        self.assertEqual(app._meta.models_module.__name__, 'model_app.models')
+        self.assertEqual(mod.__name__, 'model_app.models')
+        self.assertEqual(app.__class__.__bases__, (MyOtherApp,))
+        self.assertEqual(app._meta.models_path, 'model_app.models')
+        self.assertEqual(app._meta.db_prefix, 'nomodel_app')
+        self.assertEqual(app._meta.verbose_name, 'model_app')
+
+    def test_with_complicated_inheritance(self):
+        from model_app.app import MySecondApp, YetAnotherApp
+        from django.apps import App
+        mod = cache.load_app('model_app.app.MyThirdApp')
+        app = cache.loaded_apps[0]
+        self.assertEqual(app._meta.name, 'model_app')
+        self.assertEqual(app._meta.models_module.__name__, 'model_app.yetanother')
+        self.assertEqual(mod.__name__, 'model_app.yetanother')
+        self.assertEqual(app.__class__.__bases__, (YetAnotherApp, MySecondApp))
+        self.assertEqual(app._meta.models_path, 'model_app.yetanother')
+        self.assertEqual(app._meta.db_prefix, 'nomodel_app')
+        self.assertEqual(app._meta.verbose_name, 'model_app')
+
+    def test_with_custom_models(self):
+        """
+        Test that custom models are imported correctly, if the App specifies
+        an models_path attribute
+        """
+        from model_app.app import MyApp
+        mod = cache.load_app('model_app.app.MyApp', can_postpone=False)
+        app = cache.loaded_apps[0]
+        self.assertEqual(app._meta.models_module.__name__, 'model_app.othermodels')
+        self.assertTrue(isinstance(app, MyApp))
+        self.assertEqual(mod.__name__, 'model_app.othermodels')
+
+    def test_without_models(self):
+        """
+        Test that an app instance is created even when there are
+        no models provided
+        """
+        mod = cache.load_app('nomodel_app')
+        app = cache.loaded_apps[0]
+        self.assertEqual(len(cache.loaded_apps), 1)
+        self.assertEqual(app._meta.name, 'nomodel_app')
+        self.assertEqual(mod, None)
+
+    def test_loading_the_same_app_twice(self):
+        """
+        Test that loading the same app twice results in only one app instance
+        being created
+        """
+        mod = cache.load_app('model_app')
+        mod2 = cache.load_app('model_app')
+        self.assertEqual(len(cache.loaded_apps), 1)
+        self.assertEqual(mod.__name__, 'model_app.models')
+        self.assertEqual(mod2.__name__, 'model_app.models')
+
+    def test_importerror(self):
+        """
+        Test that an ImportError exception is raised if a package cannot
+        be imported
+        """
+        self.assertRaises(ImportError, cache.load_app, 'garageland')
+
+
+class RegisterModelsTests(AppCacheTestCase):
+    """Tests for the register_models function"""
+
+    def test_seeded_cache(self):
+        """
+        Test that the models are attached to the correct app instance
+        in a seeded cache
+        """
+        settings.INSTALLED_APPS = ('model_app',)
+        cache._populate()
+        self.assertTrue(cache.app_cache_ready())
+        app_models = cache.loaded_apps[0]._meta.models.values()
+        self.assertEqual(len(app_models), 1)
+        self.assertEqual(app_models[0].__name__, 'Person')
+
+    def test_seeded_cache_invalid_app(self):
+        """
+        Test that registering models with an app that doesn't have an app
+        instance works
+        """
+        settings.INSTALLED_APPS = ('model_app',)
+        cache._populate()
+        self.assertTrue(cache.app_cache_ready())
+        from model_app.models import Person
+        cache.register_models('model_app_NONEXISTENT', *(Person,))
+        self.assertEquals(cache.app_models['model_app_NONEXISTENT']['person'], Person)
+
+    def test_unseeded_cache(self):
+        """
+        Test that models can be registered with an unseeded cache
+        """
+        from model_app.models import Person
+        self.assertFalse(cache.app_cache_ready())
+        self.assertEquals(cache.app_models['model_app']['person'], Person)
+
+
+class FindAppTests(AppCacheTestCase):
+    """Tests for the find_app function"""
+
+    def test_seeded(self):
+        """
+        Test that the correct app is returned when the cache is seeded
+        """
+        from django.apps import App
+        settings.INSTALLED_APPS = ('model_app',)
+        cache._populate()
+        self.assertTrue(cache.app_cache_ready())
+        app = cache.find_app('model_app')
+        self.assertEquals(app._meta.name, 'model_app')
+        self.assertTrue(isinstance(app, App))
+        self.assertEquals(app.__repr__(), '<App: model_app>')
+
+    def test_seeded_invalid(self):
+        """
+        Test that None is returned if an app could not be found
+        """
+        settings.INSTALLED_APPS = ('model_app',)
+        cache._populate()
+        self.assertTrue(cache.app_cache_ready())
+        app = cache.find_app('model_app_NOTVALID')
+        self.assertEquals(app, None)
+
+    def test_unseeded(self):
+        """
+        Test that the correct app is returned when the cache is unseeded
+        """
+        from django.apps import App
+        cache.load_app('model_app')
+        self.assertFalse(cache.app_cache_ready())
+        app = cache.find_app('model_app')
+        self.assertEquals(app._meta.name, 'model_app')
+        self.assertTrue(isinstance(app, App))
+
+    def test_option_override(self):
+        """
+        Tests that options of the app can be overridden in the settings
+        """
+        settings.INSTALLED_APPS = (
+            ('django.contrib.admin', {
+                'spam': 'spam',
+            }),
+            ('model_app.app.MyOverrideApp', {
+                'db_prefix': 'foobar_prefix',
+                'eggs': 'eggs',
+            }),
+        )
+        cache._populate()
+        admin = cache.find_app('admin')
+        self.assertRaises(AttributeError, lambda: admin._meta.spam)
+        self.assertEquals(admin.spam, 'spam')
+        model_app = cache.find_app('model_app')
+        self.assertEquals(model_app._meta.db_prefix, 'foobar_prefix')
+        self.assertEquals(model_app.eggs, 'eggs')
+
+    def test_conflicting_option_override(self):
+        """
+        Tests that when overrdiding the db_prefix option in the settings
+        it still throws an exception
+        """
+        settings.INSTALLED_APPS = (
+            'nomodel_app.app.MyApp',
+            ('model_app.app.MyOtherApp', {
+                'db_prefix': 'nomodel_app',
+            }),
+        )
+        self.assertRaises(ImproperlyConfigured, cache._populate)
+
+
+class SignalTests(AppCacheTestCase):
+    """Tests for the signals"""
+
+    def setUp(self):
+        super(SignalTests, self).setUp()
+        self.signal_fired = False
+
+    def test_app_loaded(self):
+        """
+        Test the app_loaded signal
+        """
+        # connect the callback before the cache is initialized
+        def app_loaded_callback(sender, app, **kwargs):
+            self.assertEqual(app._meta.name, 'model_app')
+            self.signal_fired = True
+        app_loaded.connect(app_loaded_callback)
+
+        settings.INSTALLED_APPS = ('model_app',)
+        cache._populate()
+        self.assertTrue(cache.app_cache_ready())
+        self.assertTrue(self.signal_fired)
+
+    def test_post_apps_loaded(self):
+        """
+        Test the post_apps_loaded signal
+        """
+        settings.INSTALLED_APPS = ('model_app', 'anothermodel_app')
+        def callback(sender, apps, **kwargs):
+            self.assertEqual(len(apps), 2)
+            self.assertEqual(apps[0]._meta.name, 'model_app')
+            self.assertEqual(apps[1]._meta.name, 'anothermodel_app')
+            self.signal_fired = True
+        post_apps_loaded.connect(callback)
+        cache._populate()
+        self.assertTrue(cache.app_cache_ready())
+        self.assertTrue(self.signal_fired)
+
+
+class EggLoadingTests(AppCacheTestCase):
+    """Tests loading apps from eggs"""
+
+    def setUp(self):
+        super(EggLoadingTests, self).setUp()
+        self.egg_dir = '%s/eggs' % os.path.abspath(os.path.dirname(__file__))
+        self.old_path = sys.path[:]
+
+    def tearDown(self):
+        super(EggLoadingTests, self).tearDown()
+        sys.path = self.old_path
+
+    def test_egg1(self):
+        """
+        Models module can be loaded from an app in an egg
+        """
+        egg_name = '%s/modelapp.egg' % self.egg_dir
+        sys.path.append(egg_name)
+        models = cache.load_app('app_with_models')
+        self.assertFalse(models is None)
+
+    def test_egg2(self):
+        """
+        Loading an app from an egg that has no models returns no models
+        (and no error)
+        """
+        egg_name = '%s/nomodelapp.egg' % self.egg_dir
+        sys.path.append(egg_name)
+        models = cache.load_app('app_no_models')
+        self.assertTrue(models is None)
+
+    def test_egg3(self):
+        """
+        Models module can be loaded from an app located under an egg's
+        top-level package
+        """
+        egg_name = '%s/omelet.egg' % self.egg_dir
+        sys.path.append(egg_name)
+        models = cache.load_app('omelet.app_with_models')
+        self.assertFalse(models is None)
+
+    def test_egg4(self):
+        """
+        Loading an app with no models from under the top-level egg package
+        generates no error
+        """
+        egg_name = '%s/omelet.egg' % self.egg_dir
+        sys.path.append(egg_name)
+        models = cache.load_app('omelet.app_no_models')
+        self.assertTrue(models is None)
+
+    def test_egg5(self):
+        """
+        Loading an app from an egg that has an import error in its models
+        module raises that error
+        """
+        egg_name = '%s/brokenapp.egg' % self.egg_dir
+        sys.path.append(egg_name)
+        self.assertRaises(ImportError, cache.load_app, 'broken_app')
+        try:
+            cache.load_app('broken_app')
+        except ImportError, e:
+            # Make sure the message is indicating the actual
+            # problem in the broken app.
+            self.assertTrue("modelz" in e.args[0])
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/regressiontests/app_loading/eggs/brokenapp.egg b/tests/regressiontests/app_loading/eggs/brokenapp.egg
deleted file mode 100755
index 8aca671..0000000
Binary files a/tests/regressiontests/app_loading/eggs/brokenapp.egg and /dev/null differ
diff --git a/tests/regressiontests/app_loading/eggs/modelapp.egg b/tests/regressiontests/app_loading/eggs/modelapp.egg
deleted file mode 100755
index c2370b5..0000000
Binary files a/tests/regressiontests/app_loading/eggs/modelapp.egg and /dev/null differ
diff --git a/tests/regressiontests/app_loading/eggs/nomodelapp.egg b/tests/regressiontests/app_loading/eggs/nomodelapp.egg
deleted file mode 100755
index 5b8d217..0000000
Binary files a/tests/regressiontests/app_loading/eggs/nomodelapp.egg and /dev/null differ
diff --git a/tests/regressiontests/app_loading/eggs/omelet.egg b/tests/regressiontests/app_loading/eggs/omelet.egg
deleted file mode 100755
index bd1c687..0000000
Binary files a/tests/regressiontests/app_loading/eggs/omelet.egg and /dev/null differ
diff --git a/tests/regressiontests/app_loading/not_installed/__init__.py b/tests/regressiontests/app_loading/not_installed/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/regressiontests/app_loading/not_installed/models.py b/tests/regressiontests/app_loading/not_installed/models.py
deleted file mode 100644
index 1e4b598..0000000
--- a/tests/regressiontests/app_loading/not_installed/models.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from django.db import models
-
-
-class NotInstalledModel(models.Model):
-    pass
-
-
-class RelatedModel(models.Model):
-    not_installed = models.ForeignKey(NotInstalledModel)
-
-
-class M2MRelatedModel(models.Model):
-    not_installed = models.ManyToManyField(NotInstalledModel)
diff --git a/tests/regressiontests/app_loading/tests.py b/tests/regressiontests/app_loading/tests.py
index 749f24a..5378a66 100644
--- a/tests/regressiontests/app_loading/tests.py
+++ b/tests/regressiontests/app_loading/tests.py
@@ -1,10 +1,8 @@
-import copy
 import os
 import sys
 import time
 
 from django.conf import Settings
-from django.db.models.loading import cache, load_app, get_model, get_models
 from django.utils.unittest import TestCase
 
 
@@ -23,103 +21,3 @@ class InstalledAppsGlobbingTest(TestCase):
         if hasattr(time, "tzset") and self.OLD_TZ:
             os.environ["TZ"] = self.OLD_TZ
             time.tzset()
-
-
-class EggLoadingTest(TestCase):
-
-    def setUp(self):
-        self.old_path = sys.path[:]
-        self.egg_dir = '%s/eggs' % os.path.dirname(__file__)
-
-        # This test adds dummy applications to the app cache. These
-        # need to be removed in order to prevent bad interactions
-        # with the flush operation in other tests.
-        self.old_app_models = copy.deepcopy(cache.app_models)
-        self.old_app_store = copy.deepcopy(cache.app_store)
-
-    def tearDown(self):
-        sys.path = self.old_path
-        cache.app_models = self.old_app_models
-        cache.app_store = self.old_app_store
-
-    def test_egg1(self):
-        """Models module can be loaded from an app in an egg"""
-        egg_name = '%s/modelapp.egg' % self.egg_dir
-        sys.path.append(egg_name)
-        models = load_app('app_with_models')
-        self.assertFalse(models is None)
-
-    def test_egg2(self):
-        """Loading an app from an egg that has no models returns no models (and no error)"""
-        egg_name = '%s/nomodelapp.egg' % self.egg_dir
-        sys.path.append(egg_name)
-        models = load_app('app_no_models')
-        self.assertTrue(models is None)
-
-    def test_egg3(self):
-        """Models module can be loaded from an app located under an egg's top-level package"""
-        egg_name = '%s/omelet.egg' % self.egg_dir
-        sys.path.append(egg_name)
-        models = load_app('omelet.app_with_models')
-        self.assertFalse(models is None)
-
-    def test_egg4(self):
-        """Loading an app with no models from under the top-level egg package generates no error"""
-        egg_name = '%s/omelet.egg' % self.egg_dir
-        sys.path.append(egg_name)
-        models = load_app('omelet.app_no_models')
-        self.assertTrue(models is None)
-
-    def test_egg5(self):
-        """Loading an app from an egg that has an import error in its models module raises that error"""
-        egg_name = '%s/brokenapp.egg' % self.egg_dir
-        sys.path.append(egg_name)
-        self.assertRaises(ImportError, load_app, 'broken_app')
-        try:
-            load_app('broken_app')
-        except ImportError, e:
-            # Make sure the message is indicating the actual
-            # problem in the broken app.
-            self.assertTrue("modelz" in e.args[0])
-
-
-class GetModelsTest(TestCase):
-    def setUp(self):
-        from .not_installed import models
-        self.not_installed_module = models
-
-
-    def test_get_model_only_returns_installed_models(self):
-        self.assertEqual(
-            get_model("not_installed", "NotInstalledModel"), None)
-
-
-    def test_get_model_with_not_installed(self):
-        self.assertEqual(
-            get_model(
-                "not_installed", "NotInstalledModel", only_installed=False),
-            self.not_installed_module.NotInstalledModel)
-
-
-    def test_get_models_only_returns_installed_models(self):
-        self.assertFalse(
-            "NotInstalledModel" in
-            [m.__name__ for m in get_models()])
-
-
-    def test_get_models_with_app_label_only_returns_installed_models(self):
-        self.assertEqual(get_models(self.not_installed_module), [])
-
-
-    def test_get_models_with_not_installed(self):
-        self.assertTrue(
-            "NotInstalledModel" in [
-                m.__name__ for m in get_models(only_installed=False)])
-
-
-class NotInstalledModelsTest(TestCase):
-    def test_related_not_installed_model(self):
-        from .not_installed.models import NotInstalledModel
-        self.assertEqual(
-            set(NotInstalledModel._meta.get_all_field_names()),
-            set(["id", "relatedmodel", "m2mrelatedmodel"]))
diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py
index 0b955ad..5350b2b 100644
--- a/tests/regressiontests/i18n/tests.py
+++ b/tests/regressiontests/i18n/tests.py
@@ -7,6 +7,7 @@ import pickle
 from threading import local
 
 from django.conf import settings
+from django.apps import cache
 from django.template import Template, Context
 from django.test import TestCase, RequestFactory
 from django.utils.formats import (get_format, date_format, time_format,
@@ -618,15 +619,22 @@ class ResolutionOrderI18NTests(TestCase):
         self.assertTrue(msgstr in result, ("The string '%s' isn't in the "
             "translation of '%s'; the actual result is '%s'." % (msgstr, msgid, result)))
 
+class MockedApp(object):
+    class Meta:
+        def path(self):
+            return os.path.join(
+                os.path.dirname(os.path.abspath(__file__)), 'resolution')
+    _meta = Meta()
+
 class AppResolutionOrderI18NTests(ResolutionOrderI18NTests):
 
     def setUp(self):
-        self.old_installed_apps = settings.INSTALLED_APPS
-        settings.INSTALLED_APPS = ['regressiontests.i18n.resolution'] + list(settings.INSTALLED_APPS)
+        self.old_loaded_apps = cache.loaded_apps
+        cache.loaded_apps = [MockedApp()] + cache.loaded_apps
         super(AppResolutionOrderI18NTests, self).setUp()
 
     def tearDown(self):
-        settings.INSTALLED_APPS = self.old_installed_apps
+        cache.loaded_apps = self.old_loaded_apps
         super(AppResolutionOrderI18NTests, self).tearDown()
 
     def test_app_translation(self):
diff --git a/tests/regressiontests/staticfiles_tests/tests.py b/tests/regressiontests/staticfiles_tests/tests.py
index 4056948..4c14fb8 100644
--- a/tests/regressiontests/staticfiles_tests/tests.py
+++ b/tests/regressiontests/staticfiles_tests/tests.py
@@ -8,11 +8,11 @@ import sys
 import tempfile
 from StringIO import StringIO
 
-from django.template import loader, Context
 from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured
 from django.core.files.storage import default_storage
 from django.core.management import call_command
+from django.template import loader, Context
 from django.test import TestCase
 from django.test.utils import override_settings
 from django.utils.encoding import smart_unicode
diff --git a/tests/regressiontests/templates/loaders.py b/tests/regressiontests/templates/loaders.py
index c2ae0bd..d5738be 100644
--- a/tests/regressiontests/templates/loaders.py
+++ b/tests/regressiontests/templates/loaders.py
@@ -5,6 +5,7 @@ Note: This test requires setuptools!
 """
 
 from django.conf import settings
+from django.apps import cache
 
 if __name__ == '__main__':
     settings.configure()
@@ -54,6 +55,12 @@ def create_egg(name, resources):
     egg._resources = resources
     sys.modules[name] = egg
 
+class MockedApp(object):
+    def __init__(self, name):
+        self.name = name
+    @property
+    def _meta(self):
+        return self
 
 class EggLoaderTest(unittest.TestCase):
     def setUp(self):
@@ -64,35 +71,34 @@ class EggLoaderTest(unittest.TestCase):
             os.path.normcase('templates/y.html') : StringIO.StringIO("y"),
             os.path.normcase('templates/x.txt') : StringIO.StringIO("x"),
         })
-        self._old_installed_apps = settings.INSTALLED_APPS
-        settings.INSTALLED_APPS = []
+        self.old_loaded_apps = cache.loaded_apps
 
     def tearDown(self):
-        settings.INSTALLED_APPS = self._old_installed_apps
+        cache.loaded_apps = self.old_loaded_apps
 
     def test_empty(self):
         "Loading any template on an empty egg should fail"
-        settings.INSTALLED_APPS = ['egg_empty']
+        cache.loaded_apps = [MockedApp('egg_empty')]
         egg_loader = EggLoader()
         self.assertRaises(TemplateDoesNotExist, egg_loader.load_template_source, "not-existing.html")
 
     def test_non_existing(self):
         "Template loading fails if the template is not in the egg"
-        settings.INSTALLED_APPS = ['egg_1']
+        cache.loaded_apps = [MockedApp('egg_1')]
         egg_loader = EggLoader()
         self.assertRaises(TemplateDoesNotExist, egg_loader.load_template_source, "not-existing.html")
 
     def test_existing(self):
         "A template can be loaded from an egg"
-        settings.INSTALLED_APPS = ['egg_1']
+        cache.loaded_apps = [MockedApp('egg_1')]
         egg_loader = EggLoader()
         contents, template_name = egg_loader.load_template_source("y.html")
         self.assertEqual(contents, "y")
         self.assertEqual(template_name, "egg:egg_1:templates/y.html")
 
     def test_not_installed(self):
-        "Loading an existent template from an egg not included in INSTALLED_APPS should fail"
-        settings.INSTALLED_APPS = []
+        "Loading an existent template from an egg not included in loaded_apps should fail"
+        cache.loaded_apps = []
         egg_loader = EggLoader()
         self.assertRaises(TemplateDoesNotExist, egg_loader.load_template_source, "y.html")
 
diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py
index 1d1efe5..2ee883c 100644
--- a/tests/regressiontests/templates/tests.py
+++ b/tests/regressiontests/templates/tests.py
@@ -1,4 +1,5 @@
 # -*- coding: utf-8 -*-
+from django.apps import cache
 from django.conf import settings
 
 if __name__ == '__main__':
@@ -1613,17 +1614,24 @@ class Templates(unittest.TestCase):
             'static-statictag02': ('{% load static %}{% static base_css %}', {'base_css': 'admin/base.css'}, urljoin(settings.STATIC_URL, 'admin/base.css')),
         }
 
+class MockedApp(object):
+    def __init__(self, name):
+        self.name = name
+    @property
+    def _meta(self):
+        return self
+
 class TemplateTagLoading(unittest.TestCase):
 
     def setUp(self):
         self.old_path = sys.path[:]
-        self.old_apps = settings.INSTALLED_APPS
+        self.old_loaded_apps = cache.loaded_apps
         self.egg_dir = '%s/eggs' % os.path.dirname(__file__)
         self.old_tag_modules = template_base.templatetags_modules
         template_base.templatetags_modules = []
 
     def tearDown(self):
-        settings.INSTALLED_APPS = self.old_apps
+        cache.loaded_apps = self.old_loaded_apps
         sys.path = self.old_path
         template_base.templatetags_modules = self.old_tag_modules
 
@@ -1640,7 +1648,7 @@ class TemplateTagLoading(unittest.TestCase):
         ttext = "{% load broken_egg %}"
         egg_name = '%s/tagsegg.egg' % self.egg_dir
         sys.path.append(egg_name)
-        settings.INSTALLED_APPS = ('tagsegg',)
+        cache.loaded_apps = (MockedApp('tagsegg'),)
         self.assertRaises(template.TemplateSyntaxError, template.Template, ttext)
         try:
             template.Template(ttext)
@@ -1652,7 +1660,7 @@ class TemplateTagLoading(unittest.TestCase):
         ttext = "{% load working_egg %}"
         egg_name = '%s/tagsegg.egg' % self.egg_dir
         sys.path.append(egg_name)
-        settings.INSTALLED_APPS = ('tagsegg',)
+        cache.loaded_apps = (MockedApp('tagsegg'),)
         t = template.Template(ttext)
 
 
diff --git a/tests/runtests.py b/tests/runtests.py
index ba66d2a..b955b6b 100755
--- a/tests/runtests.py
+++ b/tests/runtests.py
@@ -136,11 +136,9 @@ def setup(verbosity, test_labels):
     # in our tests.
     settings.MANAGERS = ("admin@djangoproject.com",)
 
-    # Load all the ALWAYS_INSTALLED_APPS.
-    # (This import statement is intentionally delayed until after we
-    # access settings because of the USE_I18N dependency.)
-    from django.db.models.loading import get_apps, load_app
-    get_apps()
+    # This import statement is intentionally delayed until after we
+    # access settings because of the USE_I18N dependency.
+    from django.db.models.loading import load_app
 
     # Load all the test model apps.
     test_labels_set = set([label.split('.')[0] for label in test_labels])
