Browse Source

Fixed #21927 -- Made application and instance namespaces more distinct.

Made URL application namespaces be set in the included URLconf and
instance namespaces in the call to include(). Deprecated other ways
to set application and instance namespaces.
Marten Kenbeek 9 years ago
parent
commit
1e82094f1b

+ 2 - 2
django/conf/project_template/project_name/urls.py

@@ -13,9 +13,9 @@ Including another URLconf
     1. Add an import:  from blog import urls as blog_urls
     2. Add a URL to urlpatterns:  url(r'^blog/', include(blog_urls))
 """
-from django.conf.urls import include, url
+from django.conf.urls import url
 from django.contrib import admin
 
 urlpatterns = [
-    url(r'^admin/', include(admin.site.urls)),
+    url(r'^admin/', admin.site.urls),
 ]

+ 32 - 4
django/conf/urls/__init__.py

@@ -5,7 +5,7 @@ from django.core.urlresolvers import (RegexURLPattern,
     RegexURLResolver, LocaleRegexURLResolver)
 from django.core.exceptions import ImproperlyConfigured
 from django.utils import six
-from django.utils.deprecation import RemovedInDjango20Warning
+from django.utils.deprecation import RemovedInDjango20Warning, RemovedInDjango21Warning
 
 
 __all__ = ['handler400', 'handler403', 'handler404', 'handler500', 'include', 'patterns', 'url']
@@ -19,12 +19,29 @@ handler500 = 'django.views.defaults.server_error'
 def include(arg, namespace=None, app_name=None):
     if app_name and not namespace:
         raise ValueError('Must specify a namespace if specifying app_name.')
+    if app_name:
+        warnings.warn(
+            'The app_name argument to django.conf.urls.include() is deprecated. '
+            'Set the app_name in the included URLconf instead.',
+            RemovedInDjango21Warning, stacklevel=2
+        )
 
     if isinstance(arg, tuple):
         # callable returning a namespace hint
-        if namespace:
-            raise ImproperlyConfigured('Cannot override the namespace for a dynamic module that provides a namespace')
-        urlconf_module, app_name, namespace = arg
+        try:
+            urlconf_module, app_name = arg
+        except ValueError:
+            if namespace:
+                raise ImproperlyConfigured(
+                    'Cannot override the namespace for a dynamic module that provides a namespace'
+                )
+            warnings.warn(
+                'Passing a 3-tuple to django.conf.urls.include() is deprecated. '
+                'Pass a 2-tuple containing the list of patterns and app_name, '
+                'and provide the namespace argument to include() instead.',
+                RemovedInDjango21Warning, stacklevel=2
+            )
+            urlconf_module, app_name, namespace = arg
     else:
         # No namespace hint - use manually provided namespace
         urlconf_module = arg
@@ -32,6 +49,17 @@ def include(arg, namespace=None, app_name=None):
     if isinstance(urlconf_module, six.string_types):
         urlconf_module = import_module(urlconf_module)
     patterns = getattr(urlconf_module, 'urlpatterns', urlconf_module)
+    app_name = getattr(urlconf_module, 'app_name', app_name)
+    if namespace and not app_name:
+        warnings.warn(
+            'Specifying a namespace in django.conf.urls.include() without '
+            'providing an app_name is deprecated. Set the app_name attribute '
+            'in the included module, or pass a 2-tuple containing the list of '
+            'patterns and app_name instead.',
+            RemovedInDjango21Warning, stacklevel=2
+        )
+
+    namespace = namespace or app_name
 
     # Make sure we can iterate through the patterns (without this, some
     # testcases will break).

+ 9 - 0
docs/internals/deprecation.txt

@@ -70,6 +70,15 @@ details on these changes.
   ``django.utils.feedgenerator.RssFeed`` will be removed in favor of
   ``content_type``.
 
+* The ``app_name`` argument to :func:`~django.conf.urls.include()` will be
+  removed.
+
+* Support for passing a 3-tuple as the first argument to ``include()`` will
+  be removed.
+
+* Support for setting a URL instance namespace without an application
+  namespace will be removed.
+
 .. _deprecation-removed-in-2.0:
 
 2.0

+ 5 - 5
docs/intro/tutorial01.txt

@@ -293,15 +293,15 @@ with:
 
     urlpatterns = [
         url(r'^polls/', include('polls.urls')),
-        url(r'^admin/', include(admin.site.urls)),
+        url(r'^admin/', admin.site.urls),
     ]
 
 .. admonition:: Doesn't match what you see?
 
-    If you're seeing ``admin.autodiscover()`` before the definition of
-    ``urlpatterns``, you're probably using a version of Django that doesn't
-    match this tutorial version.  You'll want to either switch to the older
-    tutorial or the newer Django version.
+    If you're seeing ``include(admin.site.urls)`` instead of just
+    ``admin.site.urls``, you're probably using a version of Django that
+    doesn't match this tutorial version.  You'll want to either switch to the
+    older tutorial or the newer Django version.
 
 You have now wired an ``index`` view into the URLconf. Lets verify it's
 working, run the following command:

+ 9 - 7
docs/intro/tutorial03.txt

@@ -442,18 +442,20 @@ view, and so might an app on the same project that is for a blog. How does one
 make it so that Django knows which app view to create for a url when using the
 ``{% url %}`` template tag?
 
-The answer is to add namespaces to your root URLconf. In the ``mysite/urls.py``
-file, go ahead and change it to include namespacing:
+The answer is to add namespaces to your  URLconf. In the ``polls/urls.py``
+file, go ahead and add an ``app_name`` to set the application namespace:
 
 .. snippet::
-    :filename: mysite/urls.py
+    :filename: polls/urls.py
 
-    from django.conf.urls import include, url
-    from django.contrib import admin
+    from django.conf.urls import url
 
+    app_name = 'polls'
     urlpatterns = [
-        url(r'^polls/', include('polls.urls', namespace="polls")),
-        url(r'^admin/', include(admin.site.urls)),
+        url(r'^$', views.index, name='index'),
+        url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
+        url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'),
+        url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
     ]
 
 Now change your ``polls/index.html`` template from:

+ 14 - 8
docs/ref/contrib/admin/index.txt

@@ -2613,19 +2613,25 @@ Hooking ``AdminSite`` instances into your URLconf
 
 The last step in setting up the Django admin is to hook your ``AdminSite``
 instance into your URLconf. Do this by pointing a given URL at the
-``AdminSite.urls`` method.
+``AdminSite.urls`` method. It is not necessary to use
+:func:`~django.conf.urls.include()`.
 
 In this example, we register the default ``AdminSite`` instance
 ``django.contrib.admin.site`` at the URL ``/admin/`` ::
 
     # urls.py
-    from django.conf.urls import include, url
+    from django.conf.urls import url
     from django.contrib import admin
 
     urlpatterns = [
-        url(r'^admin/', include(admin.site.urls)),
+        url(r'^admin/', admin.site.urls),
     ]
 
+.. versionchanged:: 1.9
+
+    In previous versions, you would pass ``admin.site.urls`` to
+    :func:`~django.conf.urls.include()`.
+
 .. _customizing-adminsite:
 
 Customizing the :class:`AdminSite` class
@@ -2655,12 +2661,12 @@ update :file:`myproject/urls.py` to reference your :class:`AdminSite` subclass.
 .. snippet::
     :filename: myproject/urls.py
 
-    from django.conf.urls import include, url
+    from django.conf.urls import url
 
     from myapp.admin import admin_site
 
     urlpatterns = [
-        url(r'^myadmin/', include(admin_site.urls)),
+        url(r'^myadmin/', admin_site.urls),
     ]
 
 Note that you may not want autodiscovery of ``admin`` modules when using your
@@ -2684,12 +2690,12 @@ separate versions of the admin site -- using the ``AdminSite`` instances
 respectively::
 
     # urls.py
-    from django.conf.urls import include, url
+    from django.conf.urls import url
     from myproject.admin import basic_site, advanced_site
 
     urlpatterns = [
-        url(r'^basic-admin/', include(basic_site.urls)),
-        url(r'^advanced-admin/', include(advanced_site.urls)),
+        url(r'^basic-admin/', basic_site.urls),
+        url(r'^advanced-admin/', advanced_site.urls),
     ]
 
 ``AdminSite`` instances take a single argument to their constructor, their

+ 1 - 1
docs/ref/contrib/gis/tutorial.txt

@@ -749,7 +749,7 @@ Next, edit your ``urls.py`` in the ``geodjango`` application folder as follows::
     from django.contrib.gis import admin
 
     urlpatterns = [
-        url(r'^admin/', include(admin.site.urls)),
+        url(r'^admin/', admin.site.urls),
     ]
 
 Create an admin user:

+ 22 - 2
docs/ref/urls.txt

@@ -130,6 +130,7 @@ include()
 
 .. function:: include(module[, namespace=None, app_name=None])
               include(pattern_list)
+              include((pattern_list, app_namespace)[, namespace=None])
               include((pattern_list, app_namespace, instance_namespace))
 
     A function that takes a full Python import path to another URLconf module
@@ -137,9 +138,14 @@ include()
     namespace` and :term:`instance namespace` where the entries will be included
     into can also be specified.
 
+    Usually, the application namespace should be specified by the included
+    module. If an application namespace is set, the ``namespace`` argument
+    can be used to set a different instance namespace.
+
     ``include()`` also accepts as an argument either an iterable that returns
-    URL patterns or a 3-tuple containing such iterable plus the names of the
-    application and instance namespaces.
+    URL patterns, a 2-tuple containing such iterable plus the names of the
+    application namespaces, or a 3-tuple containing the iterable and the names
+    of both the application and instance namespace.
 
     :arg module: URLconf module (or module name)
     :arg namespace: Instance namespace for the URL entries being included
@@ -154,6 +160,20 @@ include()
 
 See :ref:`including-other-urlconfs` and :ref:`namespaces-and-include`.
 
+.. deprecated:: 1.9
+
+    Support for the ``app_name`` argument is deprecated and will be removed in
+    Django 2.1. Specify the ``app_name`` as explained in
+    :ref:`namespaces-and-include` instead.
+
+    Support for passing a 3-tuple is also deprecated and will be removed in
+    Django 2.1. Pass a 2-tuple containing the pattern list and application
+    namespace, and use the ``namespace`` argument instead.
+
+    Lastly, support for an instance namespace without an application namespace
+    has been deprecated and will be removed in Django 2.1. Specify the
+    application namespace or remove the instance namespace.
+
 handler400
 ----------
 

+ 39 - 0
docs/releases/1.9.txt

@@ -385,6 +385,11 @@ URLs
 
 * Regular expression lookaround assertions are now allowed in URL patterns.
 
+* The application namespace can now be set using an ``app_name`` attribute
+  on the included module or object. It can also be set by passing a 2-tuple
+  of (<list of patterns>, <application namespace>) as the first argument to
+  :func:`~django.conf.urls.include`.
+
 Validators
 ^^^^^^^^^^
 
@@ -706,6 +711,40 @@ extending. This change necessitated a new template loader API. The old
 Details about the new API can be found :ref:`in the template loader
 documentation <custom-template-loaders>`.
 
+Passing a 3-tuple or an ``app_name`` to :func:`~django.conf.urls.include()`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The instance namespace part of passing a tuple as the first argument has been
+replaced by passing the ``namespace`` argument to ``include()``. The
+``app_name`` argument to ``include()`` has been replaced by passing a 2-tuple,
+or passing an object or module with an ``app_name`` attribute.
+
+If the ``app_name`` is set in this new way, the ``namespace`` argument is no
+longer required. It will default to the value of ``app_name``.
+
+This change also means that the old way of including an ``AdminSite`` instance
+is deprecated. Instead, pass ``admin.site.urls`` directly to
+:func:`~django.conf.urls.url()`:
+
+.. snippet::
+    :filename: urls.py
+
+    from django.conf.urls import url
+    from django.contrib import admin
+
+    urlpatterns = [
+        url(r'^admin/', admin.site.urls),
+    ]
+
+URL application namespace required if setting an instance namespace
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In the past, an instance namespace without an application namespace
+would serve the same purpose as the application namespace, but it was
+impossible to reverse the patterns if there was an application namespace
+with the same name. Includes that specify an instance namespace require that
+the included URLconf sets an application namespace.
+
 Miscellaneous
 ~~~~~~~~~~~~~
 

+ 49 - 29
docs/topics/http/urls.txt

@@ -716,8 +716,8 @@ displaying polls.
     from django.conf.urls import include, url
 
     urlpatterns = [
-        url(r'^author-polls/', include('polls.urls', namespace='author-polls', app_name='polls')),
-        url(r'^publisher-polls/', include('polls.urls', namespace='publisher-polls', app_name='polls')),
+        url(r'^author-polls/', include('polls.urls', namespace='author-polls')),
+        url(r'^publisher-polls/', include('polls.urls', namespace='publisher-polls')),
     ]
 
 .. snippet::
@@ -727,6 +727,7 @@ displaying polls.
 
     from . import views
 
+    app_name = 'polls'
     urlpatterns = [
         url(r'^$', views.IndexView.as_view(), name='index'),
         url(r'^(?P<pk>\d+)/$', views.DetailView.as_view(), name='detail'),
@@ -778,25 +779,44 @@ declared last in ``urlpatterns``.
 URL namespaces and included URLconfs
 ------------------------------------
 
-URL namespaces of included URLconfs can be specified in two ways.
+Application namespaces of included URLconfs can be specified in two ways.
 
-Firstly, you can provide the :term:`application <application namespace>` and
-:term:`instance <instance namespace>` namespaces as arguments to
-:func:`~django.conf.urls.include()` when you construct your URL patterns. For
-example,::
+Firstly, you can set an ``app_name`` attribute in the included URLconf module,
+at the same level as the ``urlpatterns`` attribute. You have to pass the actual
+module, or a string reference to the module, to
+:func:`~django.conf.urls.include`, not the list of ``urlpatterns`` itself.
 
-    url(r'^polls/', include('polls.urls', namespace='author-polls', app_name='polls')),
+.. snippet::
+    :filename: polls/urls.py
+
+    from django.conf.urls import url
+
+    from . import views
+
+    app_name = 'polls'
+    urlpatterns = [
+        url(r'^$', views.IndexView.as_view(), name='index'),
+        url(r'^(?P<pk>\d+)/$', views.DetailView.as_view(), name='detail'),
+        ...
+    ]
+
+.. snippet::
+    :filename: urls.py
 
-This will include the URLs defined in ``polls.urls`` into the
-:term:`application namespace` ``'polls'``, with the :term:`instance namespace`
-``'author-polls'``.
+    from django.conf.urls import include, url
+
+    urlpatterns = [
+        url(r'^polls/', include('polls.urls')),
+    ]
+
+The URLs defined in ``polls.urls`` will have an application namespace ``polls``.
 
 Secondly, you can include an object that contains embedded namespace data. If
 you ``include()`` a list of :func:`~django.conf.urls.url` instances,
 the URLs contained in that object will be added to the global namespace.
-However, you can also ``include()`` a 3-tuple containing::
+However, you can also ``include()`` a 2-tuple containing::
 
-    (<list of url() instances>, <application namespace>, <instance namespace>)
+    (<list of url() instances>, <application namespace>)
 
 For example::
 
@@ -804,25 +824,25 @@ For example::
 
     from . import views
 
-    polls_patterns = [
+    polls_patterns = ([
         url(r'^$', views.IndexView.as_view(), name='index'),
         url(r'^(?P<pk>\d+)/$', views.DetailView.as_view(), name='detail'),
-    ]
+    ], 'polls')
+
+    url(r'^polls/', include(polls_patterns)),
 
-    url(r'^polls/', include((polls_patterns, 'polls', 'author-polls'))),
+This will include the nominated URL patterns into the given application
+namespace.
 
-This will include the nominated URL patterns into the given application and
-instance namespace.
+The instance namespace can be specified using the ``namespace`` argument to
+:func:`~django.conf.urls.include`. If the instance namespace is not specified,
+it will default to the included URLconf's application namespace. This means
+it will also be the default instance for that namespace.
 
-For example, the Django admin is deployed as instances of
-:class:`~django.contrib.admin.AdminSite`.  ``AdminSite`` objects have a ``urls``
-attribute: A 3-tuple that contains all the patterns in the corresponding admin
-site, plus the application namespace ``'admin'``, and the name of the admin
-instance. It is this ``urls`` attribute that you ``include()`` into your
-projects ``urlpatterns`` when you deploy an admin instance.
+.. versionchanged:: 1.9
 
-Be sure to pass a tuple to ``include()``. If you simply pass three arguments:
-``include(polls_patterns, 'polls', 'author-polls')``, Django won't throw an
-error but due to the signature of ``include()``, ``'polls'`` will be the
-instance namespace and ``'author-polls'`` will be the application namespace
-instead of vice versa.
+    In previous versions, you had to specify both the application namespace
+    and the instance namespace in a single place, either by passing them as
+    parameters to :func:`~django.conf.urls.include` or by including a 3-tuple
+    containing
+    ``(<list of url() instances>, <application namespace>, <instance namespace>)``.

+ 4 - 4
docs/topics/i18n/translation.txt

@@ -1285,11 +1285,11 @@ prepend the current active language code to all url patterns defined within
         url(r'^sitemap\.xml$', sitemap, name='sitemap_xml'),
     ]
 
-    news_patterns = [
+    news_patterns = ([
         url(r'^$', news_views.index, name='index'),
         url(r'^category/(?P<slug>[\w-]+)/$', news_views.category, name='category'),
         url(r'^(?P<slug>[\w-]+)/$', news_views.details, name='detail'),
-    ]
+    ], 'news')
 
     urlpatterns += i18n_patterns(
         url(r'^about/$', about_views.main, name='about'),
@@ -1343,11 +1343,11 @@ URL patterns can also be marked translatable using the
         url(r'^sitemap\.xml$', sitemap, name='sitemap_xml'),
     ]
 
-    news_patterns = [
+    news_patterns = ([
         url(r'^$', news_views.index, name='index'),
         url(_(r'^category/(?P<slug>[\w-]+)/$'), news_views.category, name='category'),
         url(r'^(?P<slug>[\w-]+)/$', news_views.details, name='detail'),
-    ]
+    ], 'news')
 
     urlpatterns += i18n_patterns(
         url(_(r'^about/$'), about_views.main, name='about'),

+ 2 - 2
tests/admin_changelist/urls.py

@@ -1,7 +1,7 @@
-from django.conf.urls import include, url
+from django.conf.urls import url
 
 from . import admin
 
 urlpatterns = [
-    url(r'^admin/', include(admin.site.urls)),
+    url(r'^admin/', admin.site.urls),
 ]

+ 2 - 2
tests/admin_custom_urls/urls.py

@@ -1,7 +1,7 @@
-from django.conf.urls import include, url
+from django.conf.urls import url
 
 from .models import site
 
 urlpatterns = [
-    url(r'^admin/', include(site.urls)),
+    url(r'^admin/', site.urls),
 ]

+ 3 - 3
tests/admin_docs/urls.py

@@ -3,12 +3,12 @@ from django.contrib import admin
 
 from . import views
 
-ns_patterns = [
+ns_patterns = ([
     url(r'^xview/func/$', views.xview_dec(views.xview), name='func'),
-]
+], 'test')
 
 urlpatterns = [
-    url(r'^admin/', include(admin.site.urls)),
+    url(r'^admin/', admin.site.urls),
     url(r'^admindocs/', include('django.contrib.admindocs.urls')),
     url(r'^', include(ns_patterns, namespace='test')),
     url(r'^xview/func/$', views.xview_dec(views.xview)),

+ 2 - 2
tests/admin_inlines/urls.py

@@ -1,7 +1,7 @@
-from django.conf.urls import include, url
+from django.conf.urls import url
 
 from . import admin
 
 urlpatterns = [
-    url(r'^admin/', include(admin.site.urls)),
+    url(r'^admin/', admin.site.urls),
 ]

+ 2 - 2
tests/admin_views/test_adminsite.py

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
 
 import datetime
 
-from django.conf.urls import include, url
+from django.conf.urls import url
 from django.contrib import admin
 from django.contrib.auth.models import User
 from django.core.urlresolvers import reverse
@@ -16,7 +16,7 @@ site.register(User)
 site.register(Article)
 
 urlpatterns = [
-    url(r'^test_admin/admin/', include(site.urls)),
+    url(r'^test_admin/admin/', site.urls),
 ]
 
 

+ 7 - 7
tests/admin_views/urls.py

@@ -6,11 +6,11 @@ urlpatterns = [
     url(r'^test_admin/admin/doc/', include('django.contrib.admindocs.urls')),
     url(r'^test_admin/admin/secure-view/$', views.secure_view, name='secure_view'),
     url(r'^test_admin/admin/secure-view2/$', views.secure_view2, name='secure_view2'),
-    url(r'^test_admin/admin/', include(admin.site.urls)),
-    url(r'^test_admin/admin2/', include(customadmin.site.urls)),
-    url(r'^test_admin/admin3/', include(admin.site.get_urls(), 'admin3', 'admin'), dict(form_url='pony')),
-    url(r'^test_admin/admin4/', include(customadmin.simple_site.urls)),
-    url(r'^test_admin/admin5/', include(admin.site2.urls)),
-    url(r'^test_admin/admin7/', include(admin.site7.urls)),
-    url(r'^test_admin/has_permission_admin/', include(custom_has_permission_admin.site.urls)),
+    url(r'^test_admin/admin/', admin.site.urls),
+    url(r'^test_admin/admin2/', customadmin.site.urls),
+    url(r'^test_admin/admin3/', (admin.site.get_urls(), 'admin', 'admin3'), dict(form_url='pony')),
+    url(r'^test_admin/admin4/', customadmin.simple_site.urls),
+    url(r'^test_admin/admin5/', admin.site2.urls),
+    url(r'^test_admin/admin7/', admin.site7.urls),
+    url(r'^test_admin/has_permission_admin/', custom_has_permission_admin.site.urls),
 ]

+ 2 - 2
tests/admin_widgets/urls.py

@@ -1,7 +1,7 @@
-from django.conf.urls import include, url
+from django.conf.urls import url
 
 from . import widgetadmin
 
 urlpatterns = [
-    url(r'^', include(widgetadmin.site.urls)),
+    url(r'^', widgetadmin.site.urls),
 ]

+ 2 - 2
tests/auth_tests/urls.py

@@ -1,4 +1,4 @@
-from django.conf.urls import include, url
+from django.conf.urls import url
 from django.contrib import admin
 from django.contrib.auth import views
 from django.contrib.auth.decorators import login_required
@@ -97,5 +97,5 @@ urlpatterns = auth_urlpatterns + [
     url(r'^userpage/(.+)/$', userpage, name="userpage"),
 
     # This line is only required to render the password reset with is_admin=True
-    url(r'^admin/', include(admin.site.urls)),
+    url(r'^admin/', admin.site.urls),
 ]

+ 2 - 2
tests/auth_tests/urls_admin.py

@@ -2,7 +2,7 @@
 Test URLs for auth admins.
 """
 
-from django.conf.urls import include, url
+from django.conf.urls import url
 from django.contrib import admin
 from django.contrib.auth.admin import GroupAdmin, UserAdmin
 from django.contrib.auth.models import Group, User
@@ -14,5 +14,5 @@ site.register(User, UserAdmin)
 site.register(Group, GroupAdmin)
 
 urlpatterns += [
-    url(r'^admin/', include(site.urls)),
+    url(r'^admin/', site.urls),
 ]

+ 2 - 2
tests/auth_tests/urls_custom_user_admin.py

@@ -1,4 +1,4 @@
-from django.conf.urls import include, url
+from django.conf.urls import url
 from django.contrib import admin
 from django.contrib.auth import get_user_model
 from django.contrib.auth.admin import UserAdmin
@@ -16,5 +16,5 @@ class CustomUserAdmin(UserAdmin):
 site.register(get_user_model(), CustomUserAdmin)
 
 urlpatterns = [
-    url(r'^admin/', include(site.urls)),
+    url(r'^admin/', site.urls),
 ]

+ 2 - 2
tests/generic_inline_admin/urls.py

@@ -1,7 +1,7 @@
-from django.conf.urls import include, url
+from django.conf.urls import url
 
 from . import admin
 
 urlpatterns = [
-    url(r'^generic_inline_admin/admin/', include(admin.site.urls)),
+    url(r'^generic_inline_admin/admin/', admin.site.urls),
 ]

+ 1 - 0
tests/i18n/patterns/urls/namespace.py

@@ -4,6 +4,7 @@ from django.views.generic import TemplateView
 
 view = TemplateView.as_view(template_name='dummy.html')
 
+app_name = 'account'
 urlpatterns = [
     url(_(r'^register/$'), view, name='register'),
     url(_(r'^register-without-slash$'), view, name='register-without-slash'),

+ 1 - 0
tests/i18n/patterns/urls/wrong_namespace.py

@@ -5,6 +5,7 @@ from django.views.generic import TemplateView
 
 view = TemplateView.as_view(template_name='dummy.html')
 
+app_name = 'account'
 urlpatterns = i18n_patterns(
     url(_(r'^register/$'), view, name='register'),
 )

+ 2 - 2
tests/proxy_models/urls.py

@@ -1,7 +1,7 @@
-from django.conf.urls import include, url
+from django.conf.urls import url
 
 from .admin import site
 
 urlpatterns = [
-    url(r'^admin/', include(site.urls)),
+    url(r'^admin/', site.urls),
 ]

+ 2 - 2
tests/timezones/urls.py

@@ -1,7 +1,7 @@
-from django.conf.urls import include, url
+from django.conf.urls import url
 
 from . import admin as tz_admin  # NOQA: register tz_admin
 
 urlpatterns = [
-    url(r'^admin/', include(tz_admin.site.urls)),
+    url(r'^admin/', tz_admin.site.urls),
 ]

+ 16 - 0
tests/urlpatterns_reverse/included_app_urls.py

@@ -0,0 +1,16 @@
+from django.conf.urls import url
+
+from . import views
+
+app_name = 'inc-app'
+urlpatterns = [
+    url(r'^normal/$', views.empty_view, name='inc-normal-view'),
+    url(r'^normal/(?P<arg1>[0-9]+)/(?P<arg2>[0-9]+)/$', views.empty_view, name='inc-normal-view'),
+
+    url(r'^\+\\\$\*/$', views.empty_view, name='inc-special-view'),
+
+    url(r'^mixed_args/([0-9]+)/(?P<arg2>[0-9]+)/$', views.empty_view, name='inc-mixed-args'),
+    url(r'^no_kwargs/([0-9]+)/([0-9]+)/$', views.empty_view, name='inc-no-kwargs'),
+
+    url(r'^view_class/(?P<arg1>[0-9]+)/(?P<arg2>[0-9]+)/$', views.view_class_instance, name='inc-view-class'),
+]

+ 9 - 14
tests/urlpatterns_reverse/namespace_urls.py

@@ -1,20 +1,7 @@
 from django.conf.urls import include, url
 
 from . import views
-
-
-class URLObject(object):
-    def __init__(self, app_name, namespace):
-        self.app_name = app_name
-        self.namespace = namespace
-
-    def urls(self):
-        return ([
-            url(r'^inner/$', views.empty_view, name='urlobject-view'),
-            url(r'^inner/(?P<arg1>[0-9]+)/(?P<arg2>[0-9]+)/$', views.empty_view, name='urlobject-view'),
-            url(r'^inner/\+\\\$\*/$', views.empty_view, name='urlobject-special-view'),
-        ], self.app_name, self.namespace)
-    urls = property(urls)
+from .tests import URLObject
 
 testobj1 = URLObject('testapp', 'test-ns1')
 testobj2 = URLObject('testapp', 'test-ns2')
@@ -23,6 +10,8 @@ default_testobj = URLObject('testapp', 'testapp')
 otherobj1 = URLObject('nodefault', 'other-ns1')
 otherobj2 = URLObject('nodefault', 'other-ns2')
 
+newappobj1 = URLObject('newapp')
+
 urlpatterns = [
     url(r'^normal/$', views.empty_view, name='normal-view'),
     url(r'^normal/(?P<arg1>[0-9]+)/(?P<arg2>[0-9]+)/$', views.empty_view, name='normal-view'),
@@ -45,6 +34,12 @@ urlpatterns = [
     url(r'^other1/', include(otherobj1.urls)),
     url(r'^other[246]/', include(otherobj2.urls)),
 
+    url(r'^newapp1/', include(newappobj1.app_urls, 'new-ns1')),
+    url(r'^new-default/', include(newappobj1.app_urls)),
+
+    url(r'^app-included[135]/', include('urlpatterns_reverse.included_app_urls', namespace='app-ns1')),
+    url(r'^app-included2/', include('urlpatterns_reverse.included_app_urls', namespace='app-ns2')),
+
     url(r'^ns-included[135]/', include('urlpatterns_reverse.included_namespace_urls', namespace='inc-ns1')),
     url(r'^ns-included2/', include('urlpatterns_reverse.included_namespace_urls', namespace='inc-ns2')),
 

+ 105 - 3
tests/urlpatterns_reverse/tests.py

@@ -10,7 +10,7 @@ import unittest
 from admin_scripts.tests import AdminScriptTestCase
 
 from django.conf import settings
-from django.conf.urls import include
+from django.conf.urls import include, url
 from django.contrib.auth.models import User
 from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
 from django.core.urlresolvers import (
@@ -26,7 +26,9 @@ from django.test import (
 )
 from django.test.utils import override_script_prefix
 from django.utils import six
-from django.utils.deprecation import RemovedInDjango20Warning
+from django.utils.deprecation import (
+    RemovedInDjango20Warning, RemovedInDjango21Warning,
+)
 
 from . import middleware, urlconf_outer, views
 from .views import empty_view
@@ -184,6 +186,26 @@ test_data = (
 )
 
 
+class URLObject(object):
+    urlpatterns = [
+        url(r'^inner/$', views.empty_view, name='urlobject-view'),
+        url(r'^inner/(?P<arg1>[0-9]+)/(?P<arg2>[0-9]+)/$', views.empty_view, name='urlobject-view'),
+        url(r'^inner/\+\\\$\*/$', views.empty_view, name='urlobject-special-view'),
+    ]
+
+    def __init__(self, app_name, namespace=None):
+        self.app_name = app_name
+        self.namespace = namespace
+
+    @property
+    def urls(self):
+        return self.urlpatterns, self.app_name, self.namespace
+
+    @property
+    def app_urls(self):
+        return self.urlpatterns, self.app_name
+
+
 @override_settings(ROOT_URLCONF='urlpatterns_reverse.no_urls')
 class NoURLPatternsTests(SimpleTestCase):
 
@@ -281,6 +303,7 @@ class URLPatternReverse(SimpleTestCase):
 
 
 class ResolverTests(unittest.TestCase):
+    @ignore_warnings(category=RemovedInDjango21Warning)
     def test_resolver_repr(self):
         """
         Test repr of RegexURLResolver, especially when urlconf_name is a list
@@ -460,6 +483,7 @@ class ReverseShortcutTests(SimpleTestCase):
 
 
 @override_settings(ROOT_URLCONF='urlpatterns_reverse.namespace_urls')
+@ignore_warnings(category=RemovedInDjango21Warning)
 class NamespaceTests(SimpleTestCase):
 
     def test_ambiguous_object(self):
@@ -500,6 +524,20 @@ class NamespaceTests(SimpleTestCase):
         self.assertEqual('/test1/inner/42/37/', reverse('test-ns1:urlobject-view', kwargs={'arg1': 42, 'arg2': 37}))
         self.assertEqual('/test1/inner/+%5C$*/', reverse('test-ns1:urlobject-special-view'))
 
+    def test_app_object(self):
+        "Dynamic URL objects can return a (pattern, app_name) 2-tuple, and include() can set the namespace"
+        self.assertEqual('/newapp1/inner/', reverse('new-ns1:urlobject-view'))
+        self.assertEqual('/newapp1/inner/37/42/', reverse('new-ns1:urlobject-view', args=[37, 42]))
+        self.assertEqual('/newapp1/inner/42/37/', reverse('new-ns1:urlobject-view', kwargs={'arg1': 42, 'arg2': 37}))
+        self.assertEqual('/newapp1/inner/+%5C$*/', reverse('new-ns1:urlobject-special-view'))
+
+    def test_app_object_default_namespace(self):
+        "Namespace defaults to app_name when including a (pattern, app_name) 2-tuple"
+        self.assertEqual('/new-default/inner/', reverse('newapp:urlobject-view'))
+        self.assertEqual('/new-default/inner/37/42/', reverse('newapp:urlobject-view', args=[37, 42]))
+        self.assertEqual('/new-default/inner/42/37/', reverse('newapp:urlobject-view', kwargs={'arg1': 42, 'arg2': 37}))
+        self.assertEqual('/new-default/inner/+%5C$*/', reverse('newapp:urlobject-special-view'))
+
     def test_embedded_namespace_object(self):
         "Namespaces can be installed anywhere in the URL pattern tree"
         self.assertEqual('/included/test3/inner/', reverse('test-ns3:urlobject-view'))
@@ -514,6 +552,13 @@ class NamespaceTests(SimpleTestCase):
         self.assertEqual('/ns-included1/normal/42/37/', reverse('inc-ns1:inc-normal-view', kwargs={'arg1': 42, 'arg2': 37}))
         self.assertEqual('/ns-included1/+%5C$*/', reverse('inc-ns1:inc-special-view'))
 
+    def test_app_name_pattern(self):
+        "Namespaces can be applied to include()'d urlpatterns that set an app_name attribute"
+        self.assertEqual('/app-included1/normal/', reverse('app-ns1:inc-normal-view'))
+        self.assertEqual('/app-included1/normal/37/42/', reverse('app-ns1:inc-normal-view', args=[37, 42]))
+        self.assertEqual('/app-included1/normal/42/37/', reverse('app-ns1:inc-normal-view', kwargs={'arg1': 42, 'arg2': 37}))
+        self.assertEqual('/app-included1/+%5C$*/', reverse('app-ns1:inc-special-view'))
+
     def test_namespace_pattern_with_variable_prefix(self):
         "When using an include with namespaces when there is a regex variable in front of it"
         self.assertEqual('/ns-outer/42/normal/', reverse('inc-outer:inc-normal-view', kwargs={'outer': 42}))
@@ -769,6 +814,7 @@ class NoRootUrlConfTests(SimpleTestCase):
 @override_settings(ROOT_URLCONF='urlpatterns_reverse.namespace_urls')
 class ResolverMatchTests(SimpleTestCase):
 
+    @ignore_warnings(category=RemovedInDjango21Warning)
     def test_urlpattern_resolve(self):
         for path, url_name, app_name, namespace, view_name, func, args, kwargs in resolve_test_data:
             # Test legacy support for extracting "function, args, kwargs"
@@ -793,6 +839,7 @@ class ResolverMatchTests(SimpleTestCase):
             self.assertEqual(match[1], args)
             self.assertEqual(match[2], kwargs)
 
+    @ignore_warnings(category=RemovedInDjango21Warning)
     def test_resolver_match_on_request(self):
         response = self.client.get('/resolver_match/')
         resolver_match = response.resolver_match
@@ -845,10 +892,65 @@ class ViewLoadingTests(SimpleTestCase):
 
 
 class IncludeTests(SimpleTestCase):
+    url_patterns = [
+        url(r'^inner/$', views.empty_view, name='urlobject-view'),
+        url(r'^inner/(?P<arg1>[0-9]+)/(?P<arg2>[0-9]+)/$', views.empty_view, name='urlobject-view'),
+        url(r'^inner/\+\\\$\*/$', views.empty_view, name='urlobject-special-view'),
+    ]
+    app_urls = URLObject('inc-app')
+
     def test_include_app_name_but_no_namespace(self):
         msg = "Must specify a namespace if specifying app_name."
         with self.assertRaisesMessage(ValueError, msg):
-            include('urls', app_name='bar')
+            include(self.url_patterns, app_name='bar')
+
+    def test_include_urls(self):
+        self.assertEqual(include(self.url_patterns), (self.url_patterns, None, None))
+
+    @ignore_warnings(category=RemovedInDjango21Warning)
+    def test_include_namespace(self):
+        # no app_name -> deprecated
+        self.assertEqual(include(self.url_patterns, 'namespace'), (self.url_patterns, None, 'namespace'))
+
+    @ignore_warnings(category=RemovedInDjango21Warning)
+    def test_include_namespace_app_name(self):
+        # app_name argument to include -> deprecated
+        self.assertEqual(
+            include(self.url_patterns, 'namespace', 'app_name'),
+            (self.url_patterns, 'app_name', 'namespace')
+        )
+
+    @ignore_warnings(category=RemovedInDjango21Warning)
+    def test_include_3_tuple(self):
+        # 3-tuple -> deprecated
+        self.assertEqual(
+            include((self.url_patterns, 'app_name', 'namespace')),
+            (self.url_patterns, 'app_name', 'namespace')
+        )
+
+    def test_include_2_tuple(self):
+        self.assertEqual(
+            include((self.url_patterns, 'app_name')),
+            (self.url_patterns, 'app_name', 'app_name')
+        )
+
+    def test_include_2_tuple_namespace(self):
+        self.assertEqual(
+            include((self.url_patterns, 'app_name'), namespace='namespace'),
+            (self.url_patterns, 'app_name', 'namespace')
+        )
+
+    def test_include_app_name(self):
+        self.assertEqual(
+            include(self.app_urls),
+            (self.app_urls, 'inc-app', 'inc-app')
+        )
+
+    def test_include_app_name_namespace(self):
+        self.assertEqual(
+            include(self.app_urls, 'namespace'),
+            (self.app_urls, 'inc-app', 'namespace')
+        )
 
 
 @override_settings(ROOT_URLCONF='urlpatterns_reverse.urls')

+ 2 - 2
tests/view_tests/default_urls.py

@@ -1,7 +1,7 @@
-from django.conf.urls import include, url
+from django.conf.urls import url
 from django.contrib import admin
 
 urlpatterns = [
     # This is the same as in the default project template
-    url(r'^admin/', include(admin.site.urls)),
+    url(r'^admin/', admin.site.urls),
 ]