From df41b5a05d4e00e80e73afe629072e37873e767a Mon Sep 17 00:00:00 2001 From: Sjoerd Job Postmus Date: Thu, 20 Oct 2016 19:29:04 +0200 Subject: [PATCH] Fixed #28593 -- Added a simplified URL routing syntax per DEP 0201. Thanks Aymeric Augustin for shepherding the DEP and patch review. Thanks Marten Kenbeek and Tim Graham for contributing to the code. Thanks Tom Christie, Shai Berger, and Tim Graham for the docs. --- AUTHORS | 1 + .../project_template/project_name/urls.py-tpl | 12 +- django/conf/urls/__init__.py | 11 +- django/conf/urls/i18n.py | 16 +- django/conf/urls/static.py | 4 +- django/contrib/admin/options.py | 19 +- django/contrib/admin/sites.py | 34 +- django/contrib/admindocs/urls.py | 56 ++- django/contrib/admindocs/views.py | 5 +- django/contrib/auth/admin.py | 7 +- django/contrib/auth/urls.py | 19 +- django/contrib/flatpages/urls.py | 4 +- django/core/checks/urls.py | 4 +- django/template/defaulttags.py | 8 +- django/urls/__init__.py | 20 +- django/urls/conf.py | 31 +- django/urls/converters.py | 70 +++ django/urls/resolvers.py | 445 +++++++++++------- django/views/generic/dates.py | 2 +- django/views/templates/technical_404.html | 2 +- docs/internals/deprecation.txt | 9 +- docs/intro/overview.txt | 29 +- docs/intro/reusable-apps.txt | 2 +- docs/intro/tutorial01.txt | 91 ++-- docs/intro/tutorial03.txt | 62 ++- docs/intro/tutorial04.txt | 16 +- docs/ref/checks.txt | 16 +- docs/ref/class-based-views/base.txt | 16 +- .../class-based-views/generic-date-based.txt | 64 +-- .../ref/class-based-views/generic-display.txt | 8 +- docs/ref/class-based-views/index.txt | 2 +- .../mixins-multiple-object.txt | 2 +- docs/ref/contrib/admin/admindocs.txt | 4 +- docs/ref/contrib/admin/index.txt | 44 +- docs/ref/contrib/flatpages.txt | 14 +- docs/ref/contrib/gis/tutorial.txt | 4 +- docs/ref/contrib/sitemaps.txt | 46 +- docs/ref/contrib/staticfiles.txt | 3 +- docs/ref/contrib/syndication.txt | 12 +- docs/ref/templates/builtins.txt | 18 +- docs/ref/urlresolvers.txt | 2 +- docs/ref/urls.txt | 118 +++-- docs/ref/utils.txt | 4 +- docs/ref/views.txt | 3 +- docs/releases/1.4.txt | 5 +- docs/releases/1.9.txt | 6 +- docs/releases/2.0.txt | 26 + docs/topics/auth/default.txt | 12 +- docs/topics/cache.txt | 6 +- .../class-based-views/generic-display.txt | 14 +- .../class-based-views/generic-editing.txt | 8 +- docs/topics/class-based-views/index.txt | 12 +- docs/topics/class-based-views/intro.txt | 10 +- docs/topics/class-based-views/mixins.txt | 4 +- docs/topics/http/urls.txt | 434 +++++++++-------- docs/topics/i18n/translation.txt | 52 +- tests/check_framework/test_urls.py | 43 +- .../urls/beginning_with_slash.py | 4 +- .../urls/include_contains_tuple.py | 5 + tests/generic_views/urls.py | 428 +++++++---------- .../patterns/locale/nl/LC_MESSAGES/django.mo | Bin 697 -> 752 bytes .../patterns/locale/nl/LC_MESSAGES/django.po | 4 + tests/i18n/patterns/tests.py | 16 + tests/i18n/patterns/urls/namespace.py | 2 + tests/urlpatterns/__init__.py | 0 tests/urlpatterns/converter_urls.py | 8 + tests/urlpatterns/converters.py | 38 ++ tests/urlpatterns/path_base64_urls.py | 9 + tests/urlpatterns/path_dynamic_urls.py | 9 + tests/urlpatterns/path_urls.py | 15 + tests/urlpatterns/tests.py | 165 +++++++ tests/urlpatterns/views.py | 5 + ...vider.py => test_localeregexdescriptor.py} | 13 +- tests/urlpatterns_reverse/tests.py | 35 +- tests/view_tests/tests/test_debug.py | 7 + tests/view_tests/tests/test_static.py | 2 +- tests/view_tests/urls.py | 12 +- 77 files changed, 1663 insertions(+), 1105 deletions(-) create mode 100644 django/urls/converters.py create mode 100644 tests/check_framework/urls/include_contains_tuple.py create mode 100644 tests/urlpatterns/__init__.py create mode 100644 tests/urlpatterns/converter_urls.py create mode 100644 tests/urlpatterns/converters.py create mode 100644 tests/urlpatterns/path_base64_urls.py create mode 100644 tests/urlpatterns/path_dynamic_urls.py create mode 100644 tests/urlpatterns/path_urls.py create mode 100644 tests/urlpatterns/tests.py create mode 100644 tests/urlpatterns/views.py rename tests/urlpatterns_reverse/{test_localeregexprovider.py => test_localeregexdescriptor.py} (83%) diff --git a/AUTHORS b/AUTHORS index bd194f7f3f..65d1af7105 100644 --- a/AUTHORS +++ b/AUTHORS @@ -732,6 +732,7 @@ answer newbie questions, and generally made Django that much better: Simon Meers Simon Williams Simon Willison + Sjoerd Job Postmus Slawek Mikula sloonz smurf@smurf.noris.de diff --git a/django/conf/project_template/project_name/urls.py-tpl b/django/conf/project_template/project_name/urls.py-tpl index 30ddffb876..e23d6a92ba 100644 --- a/django/conf/project_template/project_name/urls.py-tpl +++ b/django/conf/project_template/project_name/urls.py-tpl @@ -5,17 +5,17 @@ The `urlpatterns` list routes URLs to views. For more information please see: Examples: Function views 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') + 2. Add a URL to urlpatterns: path('', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') Including another URLconf - 1. Import the include() function: from django.conf.urls import url, include - 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ -from django.conf.urls import url from django.contrib import admin +from django.urls import path urlpatterns = [ - url(r'^admin/', admin.site.urls), + path('admin/', admin.site.urls), ] diff --git a/django/conf/urls/__init__.py b/django/conf/urls/__init__.py index b7ad156122..7bda34516b 100644 --- a/django/conf/urls/__init__.py +++ b/django/conf/urls/__init__.py @@ -1,4 +1,4 @@ -from django.urls import RegexURLPattern, RegexURLResolver, include +from django.urls import include, re_path from django.views import defaults __all__ = ['handler400', 'handler403', 'handler404', 'handler500', 'include', 'url'] @@ -10,11 +10,4 @@ handler500 = defaults.server_error def url(regex, view, kwargs=None, name=None): - if isinstance(view, (list, tuple)): - # For include(...) processing. - urlconf_module, app_name, namespace = view - return RegexURLResolver(regex, urlconf_module, kwargs, app_name=app_name, namespace=namespace) - elif callable(view): - return RegexURLPattern(regex, view, kwargs, name) - else: - raise TypeError('view must be a callable or a list/tuple in the case of include().') + return re_path(regex, view, kwargs, name) diff --git a/django/conf/urls/i18n.py b/django/conf/urls/i18n.py index 70d99b4f29..325cc9e60e 100644 --- a/django/conf/urls/i18n.py +++ b/django/conf/urls/i18n.py @@ -1,8 +1,7 @@ import functools from django.conf import settings -from django.conf.urls import url -from django.urls import LocaleRegexURLResolver, get_resolver +from django.urls import LocalePrefixPattern, URLResolver, get_resolver, path from django.views.i18n import set_language @@ -13,7 +12,12 @@ def i18n_patterns(*urls, prefix_default_language=True): """ if not settings.USE_I18N: return list(urls) - return [LocaleRegexURLResolver(list(urls), prefix_default_language=prefix_default_language)] + return [ + URLResolver( + LocalePrefixPattern(prefix_default_language=prefix_default_language), + list(urls), + ) + ] @functools.lru_cache(maxsize=None) @@ -25,11 +29,11 @@ def is_language_prefix_patterns_used(urlconf): ) """ for url_pattern in get_resolver(urlconf).url_patterns: - if isinstance(url_pattern, LocaleRegexURLResolver): - return True, url_pattern.prefix_default_language + if isinstance(url_pattern.pattern, LocalePrefixPattern): + return True, url_pattern.pattern.prefix_default_language return False, False urlpatterns = [ - url(r'^setlang/$', set_language, name='set_language'), + path('setlang/', set_language, name='set_language'), ] diff --git a/django/conf/urls/static.py b/django/conf/urls/static.py index 216602229f..150f4ffd3f 100644 --- a/django/conf/urls/static.py +++ b/django/conf/urls/static.py @@ -1,8 +1,8 @@ import re from django.conf import settings -from django.conf.urls import url from django.core.exceptions import ImproperlyConfigured +from django.urls import re_path from django.views.static import serve @@ -23,5 +23,5 @@ def static(prefix, view=serve, **kwargs): # No-op if not in debug mode or a non-local prefix. return [] return [ - url(r'^%s(?P.*)$' % re.escape(prefix.lstrip('/')), view, kwargs=kwargs), + re_path(r'^%s(?P.*)$' % re.escape(prefix.lstrip('/')), view, kwargs=kwargs), ] diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 7a4ff947a8..6e4ad180ac 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -567,7 +567,7 @@ class ModelAdmin(BaseModelAdmin): return inline_instances def get_urls(self): - from django.conf.urls import url + from django.urls import path def wrap(view): def wrapper(*args, **kwargs): @@ -578,14 +578,14 @@ class ModelAdmin(BaseModelAdmin): info = self.model._meta.app_label, self.model._meta.model_name urlpatterns = [ - url(r'^$', wrap(self.changelist_view), name='%s_%s_changelist' % info), - url(r'^add/$', wrap(self.add_view), name='%s_%s_add' % info), - url(r'^autocomplete/$', wrap(self.autocomplete_view), name='%s_%s_autocomplete' % info), - url(r'^(.+)/history/$', wrap(self.history_view), name='%s_%s_history' % info), - url(r'^(.+)/delete/$', wrap(self.delete_view), name='%s_%s_delete' % info), - url(r'^(.+)/change/$', wrap(self.change_view), name='%s_%s_change' % info), + path('', wrap(self.changelist_view), name='%s_%s_changelist' % info), + path('add/', wrap(self.add_view), name='%s_%s_add' % info), + path('autocomplete/', wrap(self.autocomplete_view), name='%s_%s_autocomplete' % info), + path('/history/', wrap(self.history_view), name='%s_%s_history' % info), + path('/delete/', wrap(self.delete_view), name='%s_%s_delete' % info), + path('/change/', wrap(self.change_view), name='%s_%s_change' % info), # For backwards compatibility (was the change url before 1.9) - url(r'^(.+)/$', wrap(RedirectView.as_view( + path('/', wrap(RedirectView.as_view( pattern_name='%s:%s_%s_change' % ((self.admin_site.name,) + info) ))), ] @@ -1173,8 +1173,7 @@ class ModelAdmin(BaseModelAdmin): opts = obj._meta to_field = request.POST.get(TO_FIELD_VAR) attr = str(to_field) if to_field else opts.pk.attname - # Retrieve the `object_id` from the resolved pattern arguments. - value = request.resolver_match.args[0] + value = request.resolver_match.kwargs['object_id'] new_value = obj.serializable_value(attr) popup_response_data = json.dumps({ 'action': 'change', diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index c0767c15ee..2e37ade62e 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -196,11 +196,11 @@ class AdminSite: class MyAdminSite(AdminSite): def get_urls(self): - from django.conf.urls import url + from django.urls import path urls = super().get_urls() urls += [ - url(r'^my_view/$', self.admin_view(some_view)) + path('my_view/', self.admin_view(some_view)) ] return urls @@ -230,7 +230,7 @@ class AdminSite: return update_wrapper(inner, view) def get_urls(self): - from django.conf.urls import url, include + from django.urls import include, path, re_path # Since this module gets imported in the application's root package, # it cannot import models from other applications at the module level, # and django.contrib.contenttypes.views imports ContentType. @@ -244,15 +244,21 @@ class AdminSite: # Admin-site-wide views. urlpatterns = [ - url(r'^$', wrap(self.index), name='index'), - url(r'^login/$', self.login, name='login'), - url(r'^logout/$', wrap(self.logout), name='logout'), - url(r'^password_change/$', wrap(self.password_change, cacheable=True), name='password_change'), - url(r'^password_change/done/$', wrap(self.password_change_done, cacheable=True), - name='password_change_done'), - url(r'^jsi18n/$', wrap(self.i18n_javascript, cacheable=True), name='jsi18n'), - url(r'^r/(?P\d+)/(?P.+)/$', wrap(contenttype_views.shortcut), - name='view_on_site'), + path('', wrap(self.index), name='index'), + path('login/', self.login, name='login'), + path('logout/', wrap(self.logout), name='logout'), + path('password_change/', wrap(self.password_change, cacheable=True), name='password_change'), + path( + 'password_change/done/', + wrap(self.password_change_done, cacheable=True), + name='password_change_done', + ), + path('jsi18n/', wrap(self.i18n_javascript, cacheable=True), name='jsi18n'), + path( + 'r///', + wrap(contenttype_views.shortcut), + name='view_on_site', + ), ] # Add in each model's views, and create a list of valid URLS for the @@ -260,7 +266,7 @@ class AdminSite: valid_app_labels = [] for model, model_admin in self._registry.items(): urlpatterns += [ - url(r'^%s/%s/' % (model._meta.app_label, model._meta.model_name), include(model_admin.urls)), + path('%s/%s/' % (model._meta.app_label, model._meta.model_name), include(model_admin.urls)), ] if model._meta.app_label not in valid_app_labels: valid_app_labels.append(model._meta.app_label) @@ -270,7 +276,7 @@ class AdminSite: if valid_app_labels: regex = r'^(?P' + '|'.join(valid_app_labels) + ')/$' urlpatterns += [ - url(regex, wrap(self.app_index), name='app_list'), + re_path(regex, wrap(self.app_index), name='app_list'), ] return urlpatterns diff --git a/django/contrib/admindocs/urls.py b/django/contrib/admindocs/urls.py index bfc9648e83..bc9c3df7cf 100644 --- a/django/contrib/admindocs/urls.py +++ b/django/contrib/admindocs/urls.py @@ -1,32 +1,50 @@ -from django.conf.urls import url from django.contrib.admindocs import views +from django.urls import path, re_path urlpatterns = [ - url(r'^$', + path( + '', views.BaseAdminDocsView.as_view(template_name='admin_doc/index.html'), - name='django-admindocs-docroot'), - url(r'^bookmarklets/$', + name='django-admindocs-docroot', + ), + path( + 'bookmarklets/', views.BookmarkletsView.as_view(), - name='django-admindocs-bookmarklets'), - url(r'^tags/$', + name='django-admindocs-bookmarklets', + ), + path( + 'tags/', views.TemplateTagIndexView.as_view(), - name='django-admindocs-tags'), - url(r'^filters/$', + name='django-admindocs-tags', + ), + path( + 'filters/', views.TemplateFilterIndexView.as_view(), - name='django-admindocs-filters'), - url(r'^views/$', + name='django-admindocs-filters', + ), + path( + 'views/', views.ViewIndexView.as_view(), - name='django-admindocs-views-index'), - url(r'^views/(?P[^/]+)/$', + name='django-admindocs-views-index', + ), + path( + 'views//', views.ViewDetailView.as_view(), - name='django-admindocs-views-detail'), - url(r'^models/$', + name='django-admindocs-views-detail', + ), + path( + 'models/', views.ModelIndexView.as_view(), - name='django-admindocs-models-index'), - url(r'^models/(?P[^\.]+)\.(?P[^/]+)/$', + name='django-admindocs-models-index', + ), + re_path( + r'^models/(?P[^\.]+)\.(?P[^/]+)/$', views.ModelDetailView.as_view(), - name='django-admindocs-models-detail'), - url(r'^templates/(?P