Fixed #17304 -- Allow single-path and configured-path namespace packages as apps.

Also document the conditions under which a namespace package may or may not be
a Django app, and raise a clearer error message in those cases where it may not
be.

Thanks Aymeric for review and consultation.
This commit is contained in:
Carl Meyer 2014-01-25 19:37:05 -07:00
parent ee4b806a85
commit 966b186981
5 changed files with 120 additions and 4 deletions

View File

@ -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. <module 'django.contrib.admin.models'
# from 'django/contrib/admin/models.pyc'>. Set by import_models().

View File

@ -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. ``<module 'django.contrib.admin' from
@ -209,6 +215,32 @@ Methods
def ready(self):
MyModel = self.get_model('MyModel')
.. _namespace package:
Namespace packages as apps (Python 3.3+)
----------------------------------------
Python versions 3.3 and later support Python packages without an
``__init__.py`` file. These packages are known as "namespace packages" and may
be spread across multiple directories at different locations on ``sys.path``
(see :pep:`420`).
Django applications require a single base filesystem path where Django
(depending on configuration) will search for templates, static assets,
etc. Thus, namespace packages may only be Django applications if one of the
following is true:
1. The namespace package actually has only a single location (i.e. is not
spread across more than one directory.)
2. The :class:`~django.apps.AppConfig` class used to configure the application
has a :attr:`~django.apps.AppConfig.path` class attribute, which is the
absolute directory path Django will use as the single base path for the
application.
If neither of these conditions is met, Django will raise
:exc:`~django.core.exceptions.ImproperlyConfigured`.
Application registry
====================

View File

@ -0,0 +1,8 @@
import os
from django.apps import AppConfig
from django.utils._os import upath
class NSAppConfig(AppConfig):
name = 'nsapp'
path = upath(os.path.dirname(__file__))

View File

@ -1,10 +1,16 @@
from __future__ import absolute_import, unicode_literals
from contextlib import contextmanager
import os
import sys
from unittest import skipUnless
from django.apps import apps
from django.apps.registry import Apps
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.test import TestCase, override_settings
from django.utils._os import upath
from django.utils import six
from .default_config_app.apps import CustomConfig
@ -28,6 +34,8 @@ SOME_INSTALLED_APPS_NAMES = [
'django.contrib.auth',
] + SOME_INSTALLED_APPS[2:]
HERE = os.path.dirname(__file__)
class AppsTests(TestCase):
@ -166,3 +174,62 @@ class AppsTests(TestCase):
with self.assertRaises(LookupError):
apps.get_model("apps", "SouthPonies")
self.assertEqual(new_apps.get_model("apps", "SouthPonies"), temp_model)
@skipUnless(
sys.version_info > (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))