diff --git a/django/apps/base.py b/django/apps/base.py index 2ae40e2454..e25178e25d 100644 --- a/django/apps/base.py +++ b/django/apps/base.py @@ -39,9 +39,18 @@ class AppConfig(object): # egg. Otherwise it's a unicode on Python 2 and a str on Python 3. if not hasattr(self, 'path'): try: - self.path = upath(app_module.__path__[0]) + paths = app_module.__path__ except AttributeError: self.path = None + else: + # Convert paths to list because Python 3.3 _NamespacePath does + # not support indexing. + paths = list(paths) + if len(paths) > 1: + raise ImproperlyConfigured( + "The namespace package app %r has multiple locations, " + "which is not supported: %r" % (app_name, paths)) + self.path = upath(paths[0]) # Module containing models eg. . Set by import_models(). diff --git a/docs/ref/applications.txt b/docs/ref/applications.txt index 33f885e416..6f02a606d0 100644 --- a/docs/ref/applications.txt +++ b/docs/ref/applications.txt @@ -160,17 +160,23 @@ Configurable attributes This attribute defaults to ``label.title()``. -Read-only attributes --------------------- - .. attribute:: AppConfig.path Filesystem path to the application directory, e.g. ``'/usr/lib/python2.7/dist-packages/django/contrib/admin'``. + In most cases, Django can automatically detect and set this, but you can + also provide an explicit override as a class attribute on your + :class:`~django.apps.AppConfig` subclass. In a few situations this is + required; for instance if the app package is a `namespace package`_ with + multiple paths. + It may be ``None`` if the application isn't stored in a directory, for instance if it's loaded from an egg. +Read-only attributes +-------------------- + .. attribute:: AppConfig.module Root module for the application, e.g. `` (3, 3, 0), + "Namespace packages sans __init__.py were added in Python 3.3") +class NamespacePackageAppTests(TestCase): + # We need nsapp to be top-level so our multiple-paths tests can add another + # location for it (if its inside a normal package with an __init__.py that + # isn't possible). In order to avoid cluttering the already-full tests/ dir + # (which is on sys.path), we add these new entries to sys.path temporarily. + base_location = os.path.join(HERE, 'namespace_package_base') + other_location = os.path.join(HERE, 'namespace_package_other_base') + app_path = os.path.join(base_location, 'nsapp') + + @contextmanager + def add_to_path(self, *paths): + """Context manager to temporarily add paths to sys.path.""" + _orig_sys_path = sys.path[:] + sys.path.extend(paths) + try: + yield + finally: + sys.path = _orig_sys_path + + def test_single_path(self): + """ + A Py3.3+ namespace package can be an app if it has only one path. + """ + with self.add_to_path(self.base_location): + with self.settings(INSTALLED_APPS=['nsapp']): + app_config = apps.get_app_config('nsapp') + self.assertEqual(app_config.path, upath(self.app_path)) + + def test_multiple_paths(self): + """ + A Py3.3+ namespace package with multiple locations cannot be an app. + + (Because then we wouldn't know where to load its templates, static + assets, etc from.) + + """ + # Temporarily add two directories to sys.path that both contain + # components of the "nsapp" package. + with self.add_to_path(self.base_location, self.other_location): + with self.assertRaises(ImproperlyConfigured): + with self.settings(INSTALLED_APPS=['nsapp']): + pass + + def test_multiple_paths_explicit_path(self): + """ + Multiple locations are ok only if app-config has explicit path. + """ + # Temporarily add two directories to sys.path that both contain + # components of the "nsapp" package. + with self.add_to_path(self.base_location, self.other_location): + with self.settings(INSTALLED_APPS=['nsapp.apps.NSAppConfig']): + app_config = apps.get_app_config('nsapp') + self.assertEqual(app_config.path, upath(self.app_path))