=== modified file 'django/bin/make-messages.py'
--- django/bin/make-messages.py	
+++ django/bin/make-messages.py	
@@ -129,6 +129,22 @@
                 print "errors happened while running msguniq"
                 print errors
                 sys.exit(8)
+            # Try importing the local settings. This will work if you're
+            # in the project directory
+            sys.path.insert(0, '.')
+            try:
+                import settings
+                apps = settings.INSTALLED_APPS
+            except (ImportError, AttributeError):
+                apps = []
+            del sys.path[0]
+            # Now look for all applications which have a verbose name
+            # which is different from the default. If different, add
+            # an internationalization string.
+            for app in apps:
+                if (not isinstance(app, basestring)) and (app.verbose_name != app.path.split('.')[-1]):
+                    s = '\nmsgid "%s"\nmsgstr ""\n' % app.verbose_name
+                    msgs += s
             open(potfile, 'w').write(msgs)
             if os.path.exists(pofile):
                 (stdin, stdout, stderr) = os.popen3('msgmerge -q "%s" "%s"' % (pofile, potfile), 'b')

=== modified file 'django/conf/__init__.py'
--- django/conf/__init__.py	
+++ django/conf/__init__.py	
@@ -97,13 +97,13 @@
         # of all those apps.
         new_installed_apps = []
         for app in self.INSTALLED_APPS:
-            if app.endswith('.*'):
+            if not isinstance(app, basestring) or not app.endswith('.*'):
+                new_installed_apps.append(app)
+            else:
                 appdir = os.path.dirname(__import__(app[:-2], {}, {}, ['']).__file__)
                 for d in os.listdir(appdir):
                     if d.isalpha() and os.path.isdir(os.path.join(appdir, d)):
                         new_installed_apps.append('%s.%s' % (app[:-2], d))
-            else:
-                new_installed_apps.append(app)
         self.INSTALLED_APPS = new_installed_apps
 
         if hasattr(time, 'tzset'):
@@ -147,3 +147,26 @@
     return gettext(*args)
 
 __builtins__['_'] = first_time_gettext
+
+class app(object):
+    """Configuration directive for specifying an app."""
+    def __init__(self, path, app_label=None, verbose_name=None):
+        self.path = path
+        # if name isn't specified, get the last part of the Python dotted path
+        self.label = app_label or path.split('.')[-1]
+        self.verbose_name = verbose_name or self.label
+        self.app_module = None    # will be filled in by loading.py
+        self.models_module = None # will be filled in by loading.py
+
+    def __repr__(self):
+        return "<app: %s>" % self.path
+
+def get_installed_app_paths():
+    "Return the paths of all entries in settings.INSTALLED_APPS."
+    rv = []
+    for a in settings.INSTALLED_APPS:
+        if isinstance(a, basestring):
+            rv.append(a)
+        else:
+            rv.append(a.path)
+    return rv

=== modified file 'django/contrib/admin/templatetags/adminapplist.py'
--- django/contrib/admin/templatetags/adminapplist.py	
+++ django/contrib/admin/templatetags/adminapplist.py	
@@ -18,7 +18,7 @@
             app_models = get_models(app)
             if not app_models:
                 continue
-            app_label = app_models[0]._meta.app_label
+            app_label = app.label
 
             has_module_perms = user.has_module_perms(app_label)
 
@@ -49,7 +49,7 @@
                     model_list = [x for key, x in decorated]
 
                     app_list.append({
-                        'name': app_label.title(),
+                        'name': _(app.verbose_name),
                         'has_module_perms': has_module_perms,
                         'models': model_list,
                     })

=== modified file 'django/contrib/admin/views/doc.py'
--- django/contrib/admin/views/doc.py	
+++ django/contrib/admin/views/doc.py	
@@ -159,11 +159,11 @@
 
     # Get the model class.
     try:
-        app_mod = models.get_app(app_label)
+        app = models.get_app(app_label)
     except ImproperlyConfigured:
         raise Http404, _("App %r not found") % app_label
     model = None
-    for m in models.get_models(app_mod):
+    for m in models.get_models(app):
         if m._meta.object_name.lower() == model_name:
             model = m
             break

=== modified file 'django/contrib/auth/management.py'
--- django/contrib/auth/management.py	
+++ django/contrib/auth/management.py	
@@ -3,8 +3,8 @@
 """
 
 from django.dispatch import dispatcher
-from django.db.models import get_models, signals
-from django.contrib.auth import models as auth_app
+from django.db.models import get_models, signals, find_app
+#from django.contrib.auth import models as auth_app
 
 def _get_permission_codename(action, opts):
     return '%s_%s' % (action, opts.object_name.lower())
@@ -46,4 +46,4 @@
             break
 
 dispatcher.connect(create_permissions, signal=signals.post_syncdb)
-dispatcher.connect(create_superuser, sender=auth_app, signal=signals.post_syncdb)
+dispatcher.connect(create_superuser, sender=find_app('django.contrib.auth'), signal=signals.post_syncdb)

=== modified file 'django/contrib/sites/management.py'
--- django/contrib/sites/management.py	
+++ django/contrib/sites/management.py	
@@ -3,9 +3,9 @@
 """
 
 from django.dispatch import dispatcher
-from django.db.models import signals
+from django.db.models import signals, find_app
 from django.contrib.sites.models import Site
-from django.contrib.sites import models as site_app
+#from django.contrib.sites import models as site_app
 
 def create_default_site(app, created_models, verbosity):
     if Site in created_models:
@@ -14,4 +14,4 @@
         s = Site(domain="example.com", name="example.com")
         s.save()
 
-dispatcher.connect(create_default_site, sender=site_app, signal=signals.post_syncdb)
+dispatcher.connect(create_default_site, sender=find_app('django.contrib.sites'), signal=signals.post_syncdb)

=== modified file 'django/core/management.py'
--- django/core/management.py	
+++ django/core/management.py	
@@ -368,7 +368,7 @@
     from django.conf import settings
 
     opts = model._meta
-    app_dir = os.path.normpath(os.path.join(os.path.dirname(models.get_app(model._meta.app_label).__file__), 'sql'))
+    app_dir = os.path.normpath(os.path.join(os.path.dirname(models.get_app(model._meta.app_label).models_module.__file__), 'sql'))
     output = []
 
     # Some backends can't execute more than one SQL statement at a time,
@@ -396,7 +396,7 @@
     output = []
 
     app_models = get_models(app)
-    app_dir = os.path.normpath(os.path.join(os.path.dirname(app.__file__), 'sql'))
+    app_dir = os.path.normpath(os.path.join(os.path.dirname(app.models_module.__file__), 'sql'))
 
     for model in app_models:
         output.extend(get_custom_sql_for_model(model))
@@ -456,7 +456,7 @@
     from django.dispatch import dispatcher
     # Emit the post_sync signal for every application.
     for app in models.get_apps():
-        app_name = app.__name__.split('.')[-2]
+        app_name = app.label
         if verbosity >= 2:
             print "Running post-sync handlers for application", app_name
         dispatcher.send(signal=models.signals.post_syncdb, sender=app,
@@ -466,7 +466,7 @@
 def syncdb(verbosity=1, interactive=True):
     "Creates the database tables for all apps in INSTALLED_APPS whose tables haven't already been created."
     from django.db import connection, transaction, models, get_creation_module
-    from django.conf import settings
+    from django.conf import settings, get_installed_app_paths
 
     disable_termcolors()
 
@@ -475,7 +475,7 @@
 
     # Import the 'management' module within each installed app, to register
     # dispatcher events.
-    for app_name in settings.INSTALLED_APPS:
+    for app_name in get_installed_app_paths():
         try:
             __import__(app_name + '.management', {}, {}, [''])
         except ImportError:
@@ -496,7 +496,7 @@
 
     # Create the tables for each model
     for app in models.get_apps():
-        app_name = app.__name__.split('.')[-2]
+        app_name = app.label
         model_list = models.get_models(app)
         for model in model_list:
             # Create the model's database table, if it doesn't already exist.
@@ -519,7 +519,7 @@
     # Create the m2m tables. This must be done after all tables have been created
     # to ensure that all referred tables will exist.
     for app in models.get_apps():
-        app_name = app.__name__.split('.')[-2]
+        app_name = app.label
         model_list = models.get_models(app)
         for model in model_list:
             if model in created_models:
@@ -539,7 +539,7 @@
     # Install custom SQL for the app (but only if this
     # is a model we've just created)
     for app in models.get_apps():
-        app_name = app.__name__.split('.')[-2]
+        app_name = app.label
         for model in models.get_models(app):
             if model in created_models:
                 custom_sql = get_custom_sql_for_model(model)
@@ -556,9 +556,9 @@
                     else:
                         transaction.commit_unless_managed()
 
-    # Install SQL indicies for all newly created models
+    # Install SQL indices for all newly created models
     for app in models.get_apps():
-        app_name = app.__name__.split('.')[-2]
+        app_name = app.label
         for model in models.get_models(app):
             if model in created_models:
                 index_sql = get_sql_indexes_for_model(model)
@@ -676,7 +676,7 @@
 
 def flush(verbosity=1, interactive=True):
     "Returns all tables in the database to the same state they were in immediately after syncdb."
-    from django.conf import settings
+    from django.conf import settings, get_installed_app_paths
     from django.db import connection, transaction, models
     from django.dispatch import dispatcher
 
@@ -687,7 +687,7 @@
 
     # Import the 'management' module within each installed app, to register
     # dispatcher events.
-    for app_name in settings.INSTALLED_APPS:
+    for app_name in get_installed_app_paths():
         try:
             __import__(app_name + '.management', {}, {}, [''])
         except ImportError:
@@ -1293,9 +1293,9 @@
     from django.db.models import get_app, get_apps
 
     if len(app_labels) == 0:
-        app_list = get_apps()
+        app_list = [app.models_module for app in get_apps()]
     else:
-        app_list = [get_app(app_label) for app_label in app_labels]
+        app_list = [get_app(app_label).models_module for app_label in app_labels]
 
     test_path = settings.TEST_RUNNER.split('.')
     # Allow for Python 2.5 relative paths
@@ -1340,7 +1340,11 @@
     transaction.enter_transaction_management()
     transaction.managed(True)
 
-    app_fixtures = [os.path.join(os.path.dirname(app.__file__),'fixtures') for app in get_apps()]
+    app_fixtures = []
+    for app in get_apps():
+        if app.models_module:
+            app_fixtures.append(os.path.join(os.path.dirname(
+                                app.models_module.__file__),'fixtures'))
     for fixture_label in fixture_labels:
         parts = fixture_label.split('.')
         if len(parts) == 1:
@@ -1619,19 +1623,19 @@
         from django.db import models
         validate(silent_success=True)
         try:
-            mod_list = [models.get_app(app_label) for app_label in args[1:]]
+            app_list = [models.get_app(app_label) for app_label in args[1:]]
         except ImportError, e:
             sys.stderr.write(style.ERROR("Error: %s. Are you sure your INSTALLED_APPS setting is correct?\n" % e))
             sys.exit(1)
-        if not mod_list:
+        if not app_list:
             parser.print_usage_and_exit()
         if action not in NO_SQL_TRANSACTION:
             print style.SQL_KEYWORD("BEGIN;")
-        for mod in mod_list:
+        for app in app_list:
             if action == 'reset':
-                output = action_mapping[action](mod, options.interactive)
+                output = action_mapping[action](app, options.interactive)
             else:
-                output = action_mapping[action](mod)
+                output = action_mapping[action](app)
             if output:
                 print '\n'.join(output)
         if action not in NO_SQL_TRANSACTION:

=== modified file 'django/db/models/__init__.py'
--- django/db/models/__init__.py	
+++ django/db/models/__init__.py	
@@ -2,7 +2,7 @@
 from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
 from django.core import validators
 from django.db import backend, connection
-from django.db.models.loading import get_apps, get_app, get_models, get_model, register_models
+from django.db.models.loading import get_apps, get_app, get_models, get_model, register_models, find_app
 from django.db.models.query import Q
 from django.db.models.manager import Manager
 from django.db.models.base import Model, AdminOptions

=== modified file 'django/db/models/base.py'
--- django/db/models/base.py	
+++ django/db/models/base.py	
@@ -8,7 +8,7 @@
 from django.db.models.options import Options, AdminOptions
 from django.db import connection, backend, transaction
 from django.db.models import signals
-from django.db.models.loading import register_models, get_model
+from django.db.models.loading import register_models, get_model, get_app_label
 from django.dispatch import dispatcher
 from django.utils.datastructures import SortedDict
 from django.utils.functional import curry
@@ -45,9 +45,8 @@
         model_module = sys.modules[new_class.__module__]
 
         if getattr(new_class._meta, 'app_label', None) is None:
-            # Figure out the app_label by looking one level up.
-            # For 'django.contrib.sites.models', this would be 'sites'.
-            new_class._meta.app_label = model_module.__name__.split('.')[-2]
+            # Figure out the app_label.
+            new_class._meta.app_label = get_app_label(new_class)
 
         # Bail out early if we have already created this class.
         m = get_model(new_class._meta.app_label, name, False)

=== modified file 'django/db/models/loading.py'
--- django/db/models/loading.py	
+++ django/db/models/loading.py	
@@ -1,6 +1,6 @@
 "Utilities for loading models and the modules that contain them."
 
-from django.conf import settings
+from django.conf import settings, app
 from django.core.exceptions import ImproperlyConfigured
 import sys
 import os
@@ -18,42 +18,119 @@
 _loaded = False  # Has the contents of settings.INSTALLED_APPS been loaded?
                  # i.e., has get_apps() been called?
 
+_app_map = {}    # Map of app instances keyed by app_label.
+_mod_map = {}    # Map of app instances keyed by app module names.
+_apps = []       # List of app instances. Shadows _app_list.
+
+_set_apps = None # what INSTALLED_APPS was when _loaded was set to True
+
 def get_apps():
-    "Returns a list of all installed modules that contain models."
+    "Returns a list of all loaded applications."
     global _app_list
     global _loaded
-    if not _loaded:
+    global _app_map
+    global _mod_map
+    global _set_apps
+    global _apps
+
+    changed = (_set_apps != settings.INSTALLED_APPS)
+    if changed or not _loaded:
+        if changed:
+            _apps = []
+            _app_map = {}
+            _mod_map = {}
         _loaded = True
-        for app_name in settings.INSTALLED_APPS:
+        _set_apps = settings.INSTALLED_APPS[:]
+        # We first loop through, setting up the app instances and
+        # the relevant maps so that we can find the instances by app_label
+        # or app module name. These need to be ready because we need to be
+        # able to get the app_label e.g. for applying to model classes.
+        # If a model class is loaded indirectly without being in an
+        # installed app, the app_label used will be what it is now - the
+        # last part of the app module name.
+        # Note that _app_map and _mod_map will contain entries even
+        # when an app cannot be loaded by load_app.
+        for app_entry in settings.INSTALLED_APPS:
+            if isinstance(app_entry, basestring):
+                the_app = app(app_entry)
+            else:
+                the_app = app_entry
+            _app_map[the_app.label] = the_app
+            _mod_map[the_app.path] = the_app
+
+        for app_entry in settings.INSTALLED_APPS:
+            if isinstance(app_entry, basestring):
+                the_app = _mod_map[app_entry]
+            else:
+                the_app = app_entry
+            app_name = the_app.path
             try:
                 load_app(app_name)
+                _apps.append(the_app)
             except Exception, e:
                 # Problem importing the app
                 _app_errors[app_name] = e
-    return _app_list
+        #logger.debug("app_models: %s", _app_models)
+    return _apps
+
+def get_app_label(model_class):
+    "Returns the app label to be used for a model class."
+    global _mod_map
+    key = model_class.__module__
+    i = key.rfind('.')
+    assert i > 0, "Model class must be defined in a package sub-module"
+    key = key[:i]
+    if key in _mod_map:
+        rv = _mod_map[key].label
+    else:
+        i = key.rfind('.')
+        if i < 0:
+            rv = key
+        else:
+            rv = key[i + 1:]
+    return rv
+
+def find_app(app_path):
+    "Find an app instance, given the application path."
+    get_apps()
+    return _mod_map[app_path] # _mod_map.get(app_path, None)
 
 def get_app(app_label, emptyOK=False):
-    "Returns the module containing the models for the given app_label. If the app has no models in it and 'emptyOK' is True, returns None."
+    "Returns the app instance for the given app_label. If the app has no models in it and 'emptyOK' is True, returns None."
     get_apps() # Run get_apps() to populate the _app_list cache. Slightly hackish.
-    for app_name in settings.INSTALLED_APPS:
-        if app_label == app_name.split('.')[-1]:
-            mod = load_app(app_name)
-            if mod is None:
-                if emptyOK:
-                    return None
-            else:
-                return mod
+    if app_label not in _app_map:
+        rv = None
+    else:
+        rv = _app_map[app_label]
+    if emptyOK or rv is not None:
+        return rv
     raise ImproperlyConfigured, "App with label %s could not be found" % app_label
 
 def load_app(app_name):
-    "Loads the app with the provided fully qualified name, and returns the model module."
+    "Loads the app with the provided fully qualified name, and returns the models module."
     global _app_list
+    global _app_map
+    global _mod_map
+    global _apps
     mod = __import__(app_name, {}, {}, ['models'])
+    if app_name in _mod_map:
+        the_app = _mod_map[app_name]
+    else:
+        # An app can be loaded by load_app even before get_apps is
+        # called. In this case, we just make a new app and add it to the maps.
+        the_app = app(app_name)
+        _app_map[the_app.label] = the_app
+        _mod_map[app_name] = the_app
+        _apps.append(the_app)
+    the_app.app_module = mod
     if not hasattr(mod, 'models'):
-        return None
-    if mod.models not in _app_list:
-        _app_list.append(mod.models)
-    return mod.models
+        rv = None
+    else:
+        rv = mod.models
+        if rv not in _app_list:
+            _app_list.append(rv)
+    the_app.models_module = rv
+    return rv
 
 def get_app_errors():
     "Returns the map of known problems with the INSTALLED_APPS"
@@ -61,18 +138,18 @@
     get_apps() # Run get_apps() to populate the _app_list cache. Slightly hackish.
     return _app_errors
 
-def get_models(app_mod=None):
+def get_models(the_app=None):
     """
-    Given a module containing models, returns a list of the models. Otherwise
-    returns a list of all installed models.
+    Given an app instance, returns a list of its models. Otherwise
+    returns a list of all loaded models.
     """
-    app_list = get_apps() # Run get_apps() to populate the _app_list cache. Slightly hackish.
-    if app_mod:
-        return _app_models.get(app_mod.__name__.split('.')[-2], {}).values()
+    get_apps() # Run get_apps() to populate the _app_list cache. Slightly hackish.
+    if the_app:
+        return _app_models.get(the_app.label, {}).values()
     else:
         model_list = []
-        for app_mod in app_list:
-            model_list.extend(get_models(app_mod))
+        for the_app in _apps:
+            model_list.extend(get_models(the_app))
         return model_list
 
 def get_model(app_label, model_name, seed_cache=True):

=== modified file 'django/db/models/options.py'
--- django/db/models/options.py	
+++ django/db/models/options.py	
@@ -1,8 +1,8 @@
-from django.conf import settings
+from django.conf import settings, get_installed_app_paths
 from django.db.models.related import RelatedObject
 from django.db.models.fields.related import ManyToManyRel
 from django.db.models.fields import AutoField, FieldDoesNotExist
-from django.db.models.loading import get_models
+from django.db.models.loading import get_models, get_apps
 from django.db.models.query import orderlist2sql
 from django.db.models import Manager
 from bisect import bisect
@@ -36,7 +36,7 @@
 
     def contribute_to_class(self, cls, name):
         cls._meta = self
-        self.installed = re.sub('\.models$', '', cls.__module__) in settings.INSTALLED_APPS
+        self.installed = re.sub('\.models$', '', cls.__module__) in get_installed_app_paths()
         # First, construct the default values for these options.
         self.object_name = cls.__name__
         self.module_name = self.object_name.lower()

=== modified file 'django/template/loaders/app_directories.py'
--- django/template/loaders/app_directories.py	
+++ django/template/loaders/app_directories.py	
@@ -1,13 +1,13 @@
 # Wrapper for loading templates from "template" directories in installed app packages.
 
-from django.conf import settings
+from django.conf import settings, get_installed_app_paths
 from django.core.exceptions import ImproperlyConfigured
 from django.template import TemplateDoesNotExist
 import os
 
 # At compile time, cache the directories to search.
 app_template_dirs = []
-for app in settings.INSTALLED_APPS:
+for app in get_installed_app_paths():
     i = app.rfind('.')
     if i == -1:
         m, a = app, None

=== modified file 'django/template/loaders/eggs.py'
--- django/template/loaders/eggs.py	
+++ django/template/loaders/eggs.py	
@@ -6,7 +6,7 @@
     resource_string = None
 
 from django.template import TemplateDoesNotExist
-from django.conf import settings
+from django.conf import settings, get_installed_app_paths
 
 def load_template_source(template_name, template_dirs=None):
     """
@@ -16,7 +16,7 @@
     """
     if resource_string is not None:
         pkg_name = 'templates/' + template_name
-        for app in settings.INSTALLED_APPS:
+        for app in get_installed_app_paths():
             try:
                 return (resource_string(app, pkg_name), 'egg:%s:%s ' % (app, pkg_name))
             except:

=== modified file 'django/templatetags/__init__.py'
--- django/templatetags/__init__.py	
+++ django/templatetags/__init__.py	
@@ -1,6 +1,6 @@
-from django.conf import settings
+from django.conf import settings, get_installed_app_paths
 
-for a in settings.INSTALLED_APPS:
+for a in get_installed_app_paths():
     try:
         __path__.extend(__import__(a + '.templatetags', {}, {}, ['']).__path__)
     except ImportError:

=== modified file 'django/test/client.py'
--- django/test/client.py	
+++ django/test/client.py	
@@ -1,7 +1,7 @@
 import sys
 from cStringIO import StringIO
 from urlparse import urlparse
-from django.conf import settings
+from django.conf import settings, get_installed_app_paths
 from django.core.handlers.base import BaseHandler
 from django.core.handlers.wsgi import WSGIRequest
 from django.core.signals import got_request_exception
@@ -175,7 +175,7 @@
         if response.cookies:
             self.cookies.update(response.cookies)
 
-        if 'django.contrib.sessions' in settings.INSTALLED_APPS:
+        if 'django.contrib.sessions' in get_installed_app_paths():
             from django.contrib.sessions.middleware import SessionWrapper
             cookie = self.cookies.get(settings.SESSION_COOKIE_NAME, None)
             if cookie:

=== modified file 'django/utils/translation/trans_real.py'
--- django/utils/translation/trans_real.py	
+++ django/utils/translation/trans_real.py	
@@ -107,7 +107,7 @@
     if t is not None:
         return t
 
-    from django.conf import settings
+    from django.conf import settings, get_installed_app_paths
 
     # set up the right translation class
     klass = DjangoTranslation
@@ -160,7 +160,7 @@
         if projectpath and os.path.isdir(projectpath):
             res = _merge(projectpath)
 
-        for appname in settings.INSTALLED_APPS:
+        for appname in get_installed_app_paths():
             p = appname.rfind('.')
             if p >= 0:
                 app = getattr(__import__(appname[:p], {}, {}, [appname[p+1:]]), appname[p+1:])

=== modified file 'django/views/i18n.py'
--- django/views/i18n.py	
+++ django/views/i18n.py	
@@ -1,7 +1,7 @@
 from django import http
 from django.utils.translation import check_for_language, activate, to_locale, get_language
 from django.utils.text import javascript_quote
-from django.conf import settings
+from django.conf import settings, get_installed_app_paths
 import os
 import gettext as gettext_module
 
@@ -104,7 +104,7 @@
         packages = ['django.conf']
     if type(packages) in (str, unicode):
         packages = packages.split('+')
-    packages = [p for p in packages if p == 'django.conf' or p in settings.INSTALLED_APPS]
+    packages = [p for p in packages if p == 'django.conf' or p in get_installed_app_paths()]
     default_locale = to_locale(settings.LANGUAGE_CODE)
     locale = to_locale(get_language())
     t = {}

=== modified file 'docs/settings.txt'
--- docs/settings.txt	
+++ docs/settings.txt	
@@ -467,9 +467,29 @@
 
 Default: ``()`` (Empty tuple)
 
-A tuple of strings designating all applications that are enabled in this Django
-installation. Each string should be a full Python path to a Python package that
-contains a Django application, as created by `django-admin.py startapp`_.
+A tuple of entries designating all applications that are enabled in this Django
+installation. Each entry should be one of the following:
+
+    * A string which is a full Python path to a Python package that contains a
+      Django application, as created by `django-admin.py startapp`_.
+    * A string which is a full Python path to a Python package which contains
+      multiple applications, with an appended ``.*``. This entry is replaced
+      by entries for the contained applications. For example, a wildcard
+      entry of the form ``django.contrib.*`` would be replaced by
+      multiple entries for ``django.contrib.admin``, ``django.contrib.auth``
+      etc.
+    * An ``app`` configuration directive (to use it, you need to add
+      ``from django.conf import app`` to your ``settings`` file). This takes
+      the form ``app(path, label=None, verbose_name=None)``. The ``path``
+      argument must specify the full Python path to a Python package that
+      contains a Django application. The ``label`` argument, if specified,
+      provides a unique application label (used to disambiguate different
+      third-party applications with conflicting default label values). If this
+      value is not specified, the last portion of the application path is used
+      as the default label. The ``verbose_name`` argument, if specified, is
+      used to provide a descriptive name for the application in the admin
+      screens. If this value is not specified, the last portion of the
+      application path is used as the default value.
 
 .. _django-admin.py startapp: ../django-admin/#startapp-appname
 

=== modified file 'tests/runtests.py'
--- tests/runtests.py	
+++ tests/runtests.py	
@@ -48,16 +48,16 @@
 
     def runTest(self):
         from django.core import management
-        from django.db.models.loading import load_app
+        from django.db.models.loading import load_app, find_app
         from cStringIO import StringIO
 
         try:
             module = load_app(self.model_label)
         except Exception, e:
-            self.fail('Unable to load invalid model module')
+            self.fail('Unable to load invalid application %s: %s' % (self.model_label, e))
 
         s = StringIO()
-        count = management.get_validation_errors(s, module)
+        count = management.get_validation_errors(s, find_app(self.model_label))
         s.seek(0)
         error_log = s.read()
         actual = error_log.split('\n')

