Commit 966b1869 authored by Carl Meyer's avatar Carl Meyer
Browse files

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.
parent ee4b806a
Loading
Loading
Loading
Loading
+10 −1
Original line number Diff line number Diff line
@@ -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().
+35 −3
Original line number Diff line number Diff line
@@ -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
====================

+8 −0
Original line number Diff line number Diff line
import os

from django.apps import AppConfig
from django.utils._os import upath

class NSAppConfig(AppConfig):
    name = 'nsapp'
    path = upath(os.path.dirname(__file__))
+0 −0

Empty file added.

+67 −0
Original line number Diff line number Diff line
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))