From 1e82094f1b6690018228e688303295f83e1c3d9a Mon Sep 17 00:00:00 2001 From: Marten Kenbeek Date: Thu, 28 May 2015 17:25:52 +0200 Subject: [PATCH] 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. --- .../project_template/project_name/urls.py | 4 +- django/conf/urls/__init__.py | 36 +++++- docs/internals/deprecation.txt | 9 ++ docs/intro/tutorial01.txt | 10 +- docs/intro/tutorial03.txt | 16 +-- docs/ref/contrib/admin/index.txt | 22 ++-- docs/ref/contrib/gis/tutorial.txt | 2 +- docs/ref/urls.txt | 24 +++- docs/releases/1.9.txt | 39 +++++++ docs/topics/http/urls.txt | 78 ++++++++----- docs/topics/i18n/translation.txt | 8 +- tests/admin_changelist/urls.py | 4 +- tests/admin_custom_urls/urls.py | 4 +- tests/admin_docs/urls.py | 6 +- tests/admin_inlines/urls.py | 4 +- tests/admin_views/test_adminsite.py | 4 +- tests/admin_views/urls.py | 14 +-- tests/admin_widgets/urls.py | 4 +- tests/auth_tests/urls.py | 4 +- tests/auth_tests/urls_admin.py | 4 +- tests/auth_tests/urls_custom_user_admin.py | 4 +- tests/generic_inline_admin/urls.py | 4 +- tests/i18n/patterns/urls/namespace.py | 1 + tests/i18n/patterns/urls/wrong_namespace.py | 1 + tests/proxy_models/urls.py | 4 +- tests/timezones/urls.py | 4 +- .../urlpatterns_reverse/included_app_urls.py | 16 +++ tests/urlpatterns_reverse/namespace_urls.py | 23 ++-- tests/urlpatterns_reverse/tests.py | 108 +++++++++++++++++- tests/view_tests/default_urls.py | 4 +- 30 files changed, 352 insertions(+), 113 deletions(-) create mode 100644 tests/urlpatterns_reverse/included_app_urls.py diff --git a/django/conf/project_template/project_name/urls.py b/django/conf/project_template/project_name/urls.py index 665085b17b..c0cfc4e2a3 100644 --- a/django/conf/project_template/project_name/urls.py +++ b/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), ] diff --git a/django/conf/urls/__init__.py b/django/conf/urls/__init__.py index 6ed54930c4..64bec13ec1 100644 --- a/django/conf/urls/__init__.py +++ b/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). diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index bc55472121..1737681f88 100644 --- a/docs/internals/deprecation.txt +++ b/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 diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt index 4eb8ecca4d..6db12d118b 100644 --- a/docs/intro/tutorial01.txt +++ b/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: diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt index 5b6c3e5ce7..9da44c3caf 100644 --- a/docs/intro/tutorial03.txt +++ b/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[0-9]+)/$', views.detail, name='detail'), + url(r'^(?P[0-9]+)/results/$', views.results, name='results'), + url(r'^(?P[0-9]+)/vote/$', views.vote, name='vote'), ] Now change your ``polls/index.html`` template from: diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 091967c796..f4480d6a3b 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/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 diff --git a/docs/ref/contrib/gis/tutorial.txt b/docs/ref/contrib/gis/tutorial.txt index 05d58f0ef4..71e5304082 100644 --- a/docs/ref/contrib/gis/tutorial.txt +++ b/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: diff --git a/docs/ref/urls.txt b/docs/ref/urls.txt index 222f41a76e..0cdd29eef3 100644 --- a/docs/ref/urls.txt +++ b/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 ---------- diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt index 55906864ef..1054699bf9 100644 --- a/docs/releases/1.9.txt +++ b/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 (, ) 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 `. +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 ~~~~~~~~~~~~~ diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 607efe7308..aec8b74c3d 100644 --- a/docs/topics/http/urls.txt +++ b/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\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 ` and -:term:`instance ` 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 -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 url + + from . import views + + app_name = 'polls' + urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^(?P\d+)/$', views.DetailView.as_view(), name='detail'), + ... + ] + +.. snippet:: + :filename: urls.py + + 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:: - (, , ) + (, ) 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\d+)/$', views.DetailView.as_view(), name='detail'), - ] + ], 'polls') - url(r'^polls/', include((polls_patterns, 'polls', 'author-polls'))), + url(r'^polls/', include(polls_patterns)), -This will include the nominated URL patterns into the given application and -instance namespace. +This will include the nominated URL patterns into the given application +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. +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. -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. +.. versionchanged:: 1.9 + + 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 + ``(, , )``. diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 557204adc2..e52adf034a 100644 --- a/docs/topics/i18n/translation.txt +++ b/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[\w-]+)/$', news_views.category, name='category'), url(r'^(?P[\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[\w-]+)/$'), news_views.category, name='category'), url(r'^(?P[\w-]+)/$', news_views.details, name='detail'), - ] + ], 'news') urlpatterns += i18n_patterns( url(_(r'^about/$'), about_views.main, name='about'), diff --git a/tests/admin_changelist/urls.py b/tests/admin_changelist/urls.py index 4da326ccc4..1f553a85a9 100644 --- a/tests/admin_changelist/urls.py +++ b/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), ] diff --git a/tests/admin_custom_urls/urls.py b/tests/admin_custom_urls/urls.py index 103f9b0121..b07e1395b9 100644 --- a/tests/admin_custom_urls/urls.py +++ b/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), ] diff --git a/tests/admin_docs/urls.py b/tests/admin_docs/urls.py index 3fbfaff0c4..0bcdee4b4b 100644 --- a/tests/admin_docs/urls.py +++ b/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)), diff --git a/tests/admin_inlines/urls.py b/tests/admin_inlines/urls.py index 4da326ccc4..1f553a85a9 100644 --- a/tests/admin_inlines/urls.py +++ b/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), ] diff --git a/tests/admin_views/test_adminsite.py b/tests/admin_views/test_adminsite.py index 6e0fce57d8..575a15c23a 100644 --- a/tests/admin_views/test_adminsite.py +++ b/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), ] diff --git a/tests/admin_views/urls.py b/tests/admin_views/urls.py index 679ae916af..926eca0a2d 100644 --- a/tests/admin_views/urls.py +++ b/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), ] diff --git a/tests/admin_widgets/urls.py b/tests/admin_widgets/urls.py index 7f4721c57e..3381b2f13a 100644 --- a/tests/admin_widgets/urls.py +++ b/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), ] diff --git a/tests/auth_tests/urls.py b/tests/auth_tests/urls.py index 3c4dc6b12b..2851b451e1 100644 --- a/tests/auth_tests/urls.py +++ b/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), ] diff --git a/tests/auth_tests/urls_admin.py b/tests/auth_tests/urls_admin.py index 8d2fe3fd10..8e5b0f1f0c 100644 --- a/tests/auth_tests/urls_admin.py +++ b/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), ] diff --git a/tests/auth_tests/urls_custom_user_admin.py b/tests/auth_tests/urls_custom_user_admin.py index f8bc194ba5..dc47be68c7 100644 --- a/tests/auth_tests/urls_custom_user_admin.py +++ b/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), ] diff --git a/tests/generic_inline_admin/urls.py b/tests/generic_inline_admin/urls.py index bbabfc21a4..59f09437db 100644 --- a/tests/generic_inline_admin/urls.py +++ b/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), ] diff --git a/tests/i18n/patterns/urls/namespace.py b/tests/i18n/patterns/urls/namespace.py index 6060c57bd6..3a34c7d815 100644 --- a/tests/i18n/patterns/urls/namespace.py +++ b/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'), diff --git a/tests/i18n/patterns/urls/wrong_namespace.py b/tests/i18n/patterns/urls/wrong_namespace.py index e19c99c935..3983cb6195 100644 --- a/tests/i18n/patterns/urls/wrong_namespace.py +++ b/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'), ) diff --git a/tests/proxy_models/urls.py b/tests/proxy_models/urls.py index 854ac78040..18ade2e739 100644 --- a/tests/proxy_models/urls.py +++ b/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), ] diff --git a/tests/timezones/urls.py b/tests/timezones/urls.py index e5c88a0025..84b13b593d 100644 --- a/tests/timezones/urls.py +++ b/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), ] diff --git a/tests/urlpatterns_reverse/included_app_urls.py b/tests/urlpatterns_reverse/included_app_urls.py new file mode 100644 index 0000000000..570d6fc518 --- /dev/null +++ b/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[0-9]+)/(?P[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[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[0-9]+)/(?P[0-9]+)/$', views.view_class_instance, name='inc-view-class'), +] diff --git a/tests/urlpatterns_reverse/namespace_urls.py b/tests/urlpatterns_reverse/namespace_urls.py index 9efd0e9d64..460778860f 100644 --- a/tests/urlpatterns_reverse/namespace_urls.py +++ b/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[0-9]+)/(?P[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[0-9]+)/(?P[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')), diff --git a/tests/urlpatterns_reverse/tests.py b/tests/urlpatterns_reverse/tests.py index c9378c784e..a8aa0b2e1b 100644 --- a/tests/urlpatterns_reverse/tests.py +++ b/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[0-9]+)/(?P[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[0-9]+)/(?P[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') diff --git a/tests/view_tests/default_urls.py b/tests/view_tests/default_urls.py index 2811bf7740..f23a286305 100644 --- a/tests/view_tests/default_urls.py +++ b/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), ]