summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
Diffstat (limited to 'django')
-rw-r--r--django/conf/project_template/project_name/urls.py-tpl12
-rw-r--r--django/conf/urls/__init__.py11
-rw-r--r--django/conf/urls/i18n.py16
-rw-r--r--django/conf/urls/static.py4
-rw-r--r--django/contrib/admin/options.py19
-rw-r--r--django/contrib/admin/sites.py34
-rw-r--r--django/contrib/admindocs/urls.py56
-rw-r--r--django/contrib/admindocs/views.py5
-rw-r--r--django/contrib/auth/admin.py7
-rw-r--r--django/contrib/auth/urls.py19
-rw-r--r--django/contrib/flatpages/urls.py4
-rw-r--r--django/core/checks/urls.py4
-rw-r--r--django/template/defaulttags.py8
-rw-r--r--django/urls/__init__.py20
-rw-r--r--django/urls/conf.py31
-rw-r--r--django/urls/converters.py70
-rw-r--r--django/urls/resolvers.py415
-rw-r--r--django/views/generic/dates.py2
-rw-r--r--django/views/templates/technical_404.html2
19 files changed, 482 insertions, 257 deletions
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<path>.*)$' % re.escape(prefix.lstrip('/')), view, kwargs=kwargs),
+ re_path(r'^%s(?P<path>.*)$' % 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('<path:object_id>/history/', wrap(self.history_view), name='%s_%s_history' % info),
+ path('<path:object_id>/delete/', wrap(self.delete_view), name='%s_%s_delete' % info),
+ path('<path:object_id>/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('<path:object_id>/', 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<content_type_id>\d+)/(?P<object_id>.+)/$', 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/<int:content_type_id>/<path:object_id>/',
+ 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<app_label>' + '|'.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<view>[^/]+)/$',
+ name='django-admindocs-views-index',
+ ),
+ path(
+ 'views/<view>/',
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<app_label>[^\.]+)\.(?P<model_name>[^/]+)/$',
+ name='django-admindocs-models-index',
+ ),
+ re_path(
+ r'^models/(?P<app_label>[^\.]+)\.(?P<model_name>[^/]+)/$',
views.ModelDetailView.as_view(),
- name='django-admindocs-models-detail'),
- url(r'^templates/(?P<template>.*)/$',
+ name='django-admindocs-models-detail',
+ ),
+ path(
+ 'templates/<path:template>/',
views.TemplateDetailView.as_view(),
- name='django-admindocs-templates'),
+ name='django-admindocs-templates',
+ ),
]
diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py
index 2b96ee3e8f..e45898c025 100644
--- a/django/contrib/admindocs/views.py
+++ b/django/contrib/admindocs/views.py
@@ -401,13 +401,12 @@ def extract_views_from_urlpatterns(urlpatterns, base='', namespace=None):
continue
views.extend(extract_views_from_urlpatterns(
patterns,
- base + p.regex.pattern,
+ base + str(p.pattern),
(namespace or []) + (p.namespace and [p.namespace] or [])
))
elif hasattr(p, 'callback'):
try:
- views.append((p.callback, base + p.regex.pattern,
- namespace, p.name))
+ views.append((p.callback, base + str(p.pattern), namespace, p.name))
except ViewDoesNotExist:
continue
else:
diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py
index 3661d226a7..41df65a72e 100644
--- a/django/contrib/auth/admin.py
+++ b/django/contrib/auth/admin.py
@@ -1,5 +1,4 @@
from django.conf import settings
-from django.conf.urls import url
from django.contrib import admin, messages
from django.contrib.admin.options import IS_POPUP_VAR
from django.contrib.admin.utils import unquote
@@ -12,7 +11,7 @@ from django.core.exceptions import PermissionDenied
from django.db import router, transaction
from django.http import Http404, HttpResponseRedirect
from django.template.response import TemplateResponse
-from django.urls import reverse
+from django.urls import path, reverse
from django.utils.decorators import method_decorator
from django.utils.html import escape
from django.utils.translation import gettext, gettext_lazy as _
@@ -81,8 +80,8 @@ class UserAdmin(admin.ModelAdmin):
def get_urls(self):
return [
- url(
- r'^(.+)/password/$',
+ path(
+ '<id>/password/',
self.admin_site.admin_view(self.user_change_password),
name='auth_user_password_change',
),
diff --git a/django/contrib/auth/urls.py b/django/contrib/auth/urls.py
index 233eef8fec..c3306807e5 100644
--- a/django/contrib/auth/urls.py
+++ b/django/contrib/auth/urls.py
@@ -3,19 +3,18 @@
# It is also provided as a convenience to those who want to deploy these URLs
# elsewhere.
-from django.conf.urls import url
from django.contrib.auth import views
+from django.urls import path
urlpatterns = [
- url(r'^login/$', views.LoginView.as_view(), name='login'),
- url(r'^logout/$', views.LogoutView.as_view(), name='logout'),
+ path('login/', views.LoginView.as_view(), name='login'),
+ path('logout/', views.LogoutView.as_view(), name='logout'),
- url(r'^password_change/$', views.PasswordChangeView.as_view(), name='password_change'),
- url(r'^password_change/done/$', views.PasswordChangeDoneView.as_view(), name='password_change_done'),
+ path('password_change/', views.PasswordChangeView.as_view(), name='password_change'),
+ path('password_change/done/', views.PasswordChangeDoneView.as_view(), name='password_change_done'),
- url(r'^password_reset/$', views.PasswordResetView.as_view(), name='password_reset'),
- url(r'^password_reset/done/$', views.PasswordResetDoneView.as_view(), name='password_reset_done'),
- url(r'^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
- views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
- url(r'^reset/done/$', views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
+ path('password_reset/', views.PasswordResetView.as_view(), name='password_reset'),
+ path('password_reset/done/', views.PasswordResetDoneView.as_view(), name='password_reset_done'),
+ path('reset/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
+ path('reset/done/', views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
]
diff --git a/django/contrib/flatpages/urls.py b/django/contrib/flatpages/urls.py
index 3437e823b2..a087fe8c1d 100644
--- a/django/contrib/flatpages/urls.py
+++ b/django/contrib/flatpages/urls.py
@@ -1,6 +1,6 @@
-from django.conf.urls import url
from django.contrib.flatpages import views
+from django.urls import path
urlpatterns = [
- url(r'^(?P<url>.*)$', views.flatpage, name='django.contrib.flatpages.views.flatpage'),
+ path('<path:url>', views.flatpage, name='django.contrib.flatpages.views.flatpage'),
]
diff --git a/django/core/checks/urls.py b/django/core/checks/urls.py
index 0f65d3f3b7..e51ca3fc1f 100644
--- a/django/core/checks/urls.py
+++ b/django/core/checks/urls.py
@@ -81,13 +81,13 @@ def get_warning_for_invalid_pattern(pattern):
"have a prefix string as the first element.".format(pattern)
)
elif isinstance(pattern, tuple):
- hint = "Try using url() instead of a tuple."
+ hint = "Try using path() instead of a tuple."
else:
hint = None
return [Error(
"Your URL pattern {!r} is invalid. Ensure that urlpatterns is a list "
- "of url() instances.".format(pattern),
+ "of path() and/or re_path() instances.".format(pattern),
hint=hint,
id="urls.E004",
)]
diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
index ad2cec3432..5f56bb0d1f 100644
--- a/django/template/defaulttags.py
+++ b/django/template/defaulttags.py
@@ -1329,7 +1329,7 @@ def url(parser, token):
{% url "url_name" name1=value1 name2=value2 %}
- The first argument is a django.conf.urls.url() name. Other arguments are
+ The first argument is a URL pattern name. Other arguments are
space-separated values that will be filled in place of positional and
keyword arguments in the URL. Don't mix positional and keyword arguments.
All arguments for the URL must be present.
@@ -1337,12 +1337,12 @@ def url(parser, token):
For example, if you have a view ``app_name.views.client_details`` taking
the client's id and the corresponding line in a URLconf looks like this::
- url('^client/(\d+)/$', views.client_details, name='client-detail-view')
+ path('client/<int:id>/', views.client_details, name='client-detail-view')
and this app's URLconf is included into the project's URLconf under some
path::
- url('^clients/', include('app_name.urls'))
+ path('clients/', include('app_name.urls'))
then in a template you can create a link for a certain client like this::
@@ -1359,7 +1359,7 @@ def url(parser, token):
"""
bits = token.split_contents()
if len(bits) < 2:
- raise TemplateSyntaxError("'%s' takes at least one argument, the name of a url()." % bits[0])
+ raise TemplateSyntaxError("'%s' takes at least one argument, a URL pattern name." % bits[0])
viewname = parser.compile_filter(bits[1])
args = []
kwargs = {}
diff --git a/django/urls/__init__.py b/django/urls/__init__.py
index f6d51e8a9f..e9e32ac5b9 100644
--- a/django/urls/__init__.py
+++ b/django/urls/__init__.py
@@ -3,19 +3,21 @@ from .base import (
is_valid_path, resolve, reverse, reverse_lazy, set_script_prefix,
set_urlconf, translate_url,
)
-from .conf import include
+from .conf import include, path, re_path
+from .converters import register_converter
from .exceptions import NoReverseMatch, Resolver404
from .resolvers import (
- LocaleRegexProvider, LocaleRegexURLResolver, RegexURLPattern,
- RegexURLResolver, ResolverMatch, get_ns_resolver, get_resolver,
+ LocalePrefixPattern, ResolverMatch, URLPattern, URLResolver,
+ get_ns_resolver, get_resolver,
)
from .utils import get_callable, get_mod_func
__all__ = [
- 'LocaleRegexProvider', 'LocaleRegexURLResolver', 'NoReverseMatch',
- 'RegexURLPattern', 'RegexURLResolver', 'Resolver404', 'ResolverMatch',
- 'clear_script_prefix', 'clear_url_caches', 'get_callable', 'get_mod_func',
- 'get_ns_resolver', 'get_resolver', 'get_script_prefix', 'get_urlconf',
- 'include', 'is_valid_path', 'resolve', 'reverse', 'reverse_lazy',
- 'set_script_prefix', 'set_urlconf', 'translate_url',
+ 'LocalePrefixPattern', 'NoReverseMatch', 'URLPattern',
+ 'URLResolver', 'Resolver404', 'ResolverMatch', 'clear_script_prefix',
+ 'clear_url_caches', 'get_callable', 'get_mod_func', 'get_ns_resolver',
+ 'get_resolver', 'get_script_prefix', 'get_urlconf', 'include',
+ 'is_valid_path', 'path', 're_path', 'register_converter', 'resolve',
+ 'reverse', 'reverse_lazy', 'set_script_prefix', 'set_urlconf',
+ 'translate_url',
]
diff --git a/django/urls/conf.py b/django/urls/conf.py
index 99bff55819..119e95df41 100644
--- a/django/urls/conf.py
+++ b/django/urls/conf.py
@@ -1,9 +1,12 @@
"""Functions for use in URLsconfs."""
+from functools import partial
from importlib import import_module
from django.core.exceptions import ImproperlyConfigured
-from .resolvers import LocaleRegexURLResolver
+from .resolvers import (
+ LocalePrefixPattern, RegexPattern, RoutePattern, URLPattern, URLResolver,
+)
def include(arg, namespace=None):
@@ -43,8 +46,32 @@ def include(arg, namespace=None):
# testcases will break).
if isinstance(patterns, (list, tuple)):
for url_pattern in patterns:
- if isinstance(url_pattern, LocaleRegexURLResolver):
+ pattern = getattr(url_pattern, 'pattern', None)
+ if isinstance(pattern, LocalePrefixPattern):
raise ImproperlyConfigured(
'Using i18n_patterns in an included URLconf is not allowed.'
)
return (urlconf_module, app_name, namespace)
+
+
+def _path(route, view, kwargs=None, name=None, Pattern=None):
+ if isinstance(view, (list, tuple)):
+ # For include(...) processing.
+ pattern = Pattern(route, is_endpoint=False)
+ urlconf_module, app_name, namespace = view
+ return URLResolver(
+ pattern,
+ urlconf_module,
+ kwargs,
+ app_name=app_name,
+ namespace=namespace,
+ )
+ elif callable(view):
+ pattern = Pattern(route, name=name, is_endpoint=True)
+ return URLPattern(pattern, view, kwargs, name)
+ else:
+ raise TypeError('view must be a callable or a list/tuple in the case of include().')
+
+
+path = partial(_path, Pattern=RoutePattern)
+re_path = partial(_path, Pattern=RegexPattern)
diff --git a/django/urls/converters.py b/django/urls/converters.py
new file mode 100644
index 0000000000..eb2a61971e
--- /dev/null
+++ b/django/urls/converters.py
@@ -0,0 +1,70 @@
+import uuid
+
+from django.utils import lru_cache
+
+
+class IntConverter:
+ regex = '[0-9]+'
+
+ def to_python(self, value):
+ return int(value)
+
+ def to_url(self, value):
+ return str(value)
+
+
+class StringConverter:
+ regex = '[^/]+'
+
+ def to_python(self, value):
+ return value
+
+ def to_url(self, value):
+ return value
+
+
+class UUIDConverter:
+ regex = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
+
+ def to_python(self, value):
+ return uuid.UUID(value)
+
+ def to_url(self, value):
+ return str(value)
+
+
+class SlugConverter(StringConverter):
+ regex = '[-a-zA-Z0-9_]+'
+
+
+class PathConverter(StringConverter):
+ regex = '.+'
+
+
+DEFAULT_CONVERTERS = {
+ 'int': IntConverter(),
+ 'path': PathConverter(),
+ 'slug': SlugConverter(),
+ 'str': StringConverter(),
+ 'uuid': UUIDConverter(),
+}
+
+
+REGISTERED_CONVERTERS = {}
+
+
+def register_converter(converter, type_name):
+ REGISTERED_CONVERTERS[type_name] = converter()
+ get_converters.cache_clear()
+
+
+@lru_cache.lru_cache(maxsize=None)
+def get_converters():
+ converters = {}
+ converters.update(DEFAULT_CONVERTERS)
+ converters.update(REGISTERED_CONVERTERS)
+ return converters
+
+
+def get_converter(raw_converter):
+ return get_converters()[raw_converter]
diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py
index ecad10acea..f91e821b38 100644
--- a/django/urls/resolvers.py
+++ b/django/urls/resolvers.py
@@ -21,6 +21,7 @@ from django.utils.http import RFC3986_SUBDELIMS
from django.utils.regex_helper import normalize
from django.utils.translation import get_language
+from .converters import get_converter
from .exceptions import NoReverseMatch, Resolver404
from .utils import get_callable
@@ -64,7 +65,7 @@ def get_resolver(urlconf=None):
if urlconf is None:
from django.conf import settings
urlconf = settings.ROOT_URLCONF
- return RegexURLResolver(r'^/', urlconf)
+ return URLResolver(RegexPattern(r'^/'), urlconf)
@functools.lru_cache(maxsize=None)
@@ -72,11 +73,14 @@ def get_ns_resolver(ns_pattern, resolver):
# Build a namespaced resolver for the given parent URLconf pattern.
# This makes it possible to have captured parameters in the parent
# URLconf pattern.
- ns_resolver = RegexURLResolver(ns_pattern, resolver.url_patterns)
- return RegexURLResolver(r'^/', [ns_resolver])
+ ns_resolver = URLResolver(RegexPattern(ns_pattern), resolver.url_patterns)
+ return URLResolver(RegexPattern(r'^/'), [ns_resolver])
class LocaleRegexDescriptor:
+ def __init__(self, attr):
+ self.attr = attr
+
def __get__(self, instance, cls=None):
"""
Return a compiled regular expression based on the active language.
@@ -86,46 +90,23 @@ class LocaleRegexDescriptor:
# As a performance optimization, if the given regex string is a regular
# string (not a lazily-translated string proxy), compile it once and
# avoid per-language compilation.
- if isinstance(instance._regex, str):
- instance.__dict__['regex'] = self._compile(instance._regex)
+ pattern = getattr(instance, self.attr)
+ if isinstance(pattern, str):
+ instance.__dict__['regex'] = instance._compile(pattern)
return instance.__dict__['regex']
language_code = get_language()
if language_code not in instance._regex_dict:
- instance._regex_dict[language_code] = self._compile(str(instance._regex))
+ instance._regex_dict[language_code] = instance._compile(str(pattern))
return instance._regex_dict[language_code]
- def _compile(self, regex):
- """
- Compile and return the given regular expression.
- """
- try:
- return re.compile(regex)
- except re.error as e:
- raise ImproperlyConfigured(
- '"%s" is not a valid regular expression: %s' % (regex, e)
- )
-
-
-class LocaleRegexProvider:
- """
- A mixin to provide a default regex property which can vary by active
- language.
- """
- def __init__(self, regex):
- # regex is either a string representing a regular expression, or a
- # translatable string (using gettext_lazy) representing a regular
- # expression.
- self._regex = regex
- self._regex_dict = {}
-
- regex = LocaleRegexDescriptor()
+class CheckURLMixin:
def describe(self):
"""
Format the URL pattern for display in warning messages.
"""
- description = "'{}'".format(self.regex.pattern)
- if getattr(self, 'name', False):
+ description = "'{}'".format(self)
+ if self.name:
description += " [name='{}']".format(self.name)
return description
@@ -138,9 +119,9 @@ class LocaleRegexProvider:
# Skip check as it can be useful to start a URL pattern with a slash
# when APPEND_SLASH=False.
return []
- if (regex_pattern.startswith('/') or regex_pattern.startswith('^/')) and not regex_pattern.endswith('/'):
+ if any(regex_pattern.startswith(x) for x in ('/', '^/', '^\/')) and not regex_pattern.endswith('/'):
warning = Warning(
- "Your URL pattern {} has a regex beginning with a '/'. Remove this "
+ "Your URL pattern {} has a route beginning with a '/'. Remove this "
"slash as it is unnecessary. If this pattern is targeted in an "
"include(), ensure the include() pattern has a trailing '/'.".format(
self.describe()
@@ -152,30 +133,195 @@ class LocaleRegexProvider:
return []
-class RegexURLPattern(LocaleRegexProvider):
- def __init__(self, regex, callback, default_args=None, name=None):
- LocaleRegexProvider.__init__(self, regex)
+class RegexPattern(CheckURLMixin):
+ regex = LocaleRegexDescriptor('_regex')
+
+ def __init__(self, regex, name=None, is_endpoint=False):
+ self._regex = regex
+ self._regex_dict = {}
+ self._is_endpoint = is_endpoint
+ self.name = name
+ self.converters = {}
+
+ def match(self, path):
+ match = self.regex.search(path)
+ if match:
+ # If there are any named groups, use those as kwargs, ignoring
+ # non-named groups. Otherwise, pass all non-named arguments as
+ # positional arguments.
+ kwargs = match.groupdict()
+ args = () if kwargs else match.groups()
+ return path[match.end():], args, kwargs
+ return None
+
+ def check(self):
+ warnings = []
+ warnings.extend(self._check_pattern_startswith_slash())
+ if not self._is_endpoint:
+ warnings.extend(self._check_include_trailing_dollar())
+ return warnings
+
+ def _check_include_trailing_dollar(self):
+ regex_pattern = self.regex.pattern
+ if regex_pattern.endswith('$') and not regex_pattern.endswith(r'\$'):
+ return [Warning(
+ "Your URL pattern {} uses include with a route ending with a '$'. "
+ "Remove the dollar from the route to avoid problems including "
+ "URLs.".format(self.describe()),
+ id='urls.W001',
+ )]
+ else:
+ return []
+
+ def _compile(self, regex):
+ """Compile and return the given regular expression."""
+ try:
+ return re.compile(regex)
+ except re.error as e:
+ raise ImproperlyConfigured(
+ '"%s" is not a valid regular expression: %s' % (regex, e)
+ )
+
+ def __str__(self):
+ return self._regex
+
+
+_PATH_PARAMETER_COMPONENT_RE = re.compile(
+ '<(?:(?P<converter>[^>:]+):)?(?P<parameter>\w+)>'
+)
+
+
+def _route_to_regex(route, is_endpoint=False):
+ """
+ Convert a path pattern into a regular expression. Return the regular
+ expression and a dictionary mapping the capture names to the converters.
+ For example, 'foo/<int:pk>' returns '^foo\\/(?P<pk>[0-9]+)'
+ and {'pk': <django.urls.converters.IntConverter>}.
+ """
+ original_route = route
+ parts = ['^']
+ converters = {}
+ while True:
+ match = _PATH_PARAMETER_COMPONENT_RE.search(route)
+ if not match:
+ parts.append(re.escape(route))
+ break
+ parts.append(re.escape(route[:match.start()]))
+ route = route[match.end():]
+ parameter = match.group('parameter')
+ if not parameter.isidentifier():
+ raise ImproperlyConfigured(
+ "URL route '%s' uses parameter name %r which isn't a valid "
+ "Python identifier." % (original_route, parameter)
+ )
+ raw_converter = match.group('converter')
+ if raw_converter is None:
+ # If a converter isn't specified, the default is `str`.
+ raw_converter = 'str'
+ try:
+ converter = get_converter(raw_converter)
+ except KeyError as e:
+ raise ImproperlyConfigured(
+ "URL route '%s' uses invalid converter %s." % (original_route, e)
+ )
+ converters[parameter] = converter
+ parts.append('(?P<' + parameter + '>' + converter.regex + ')')
+ if is_endpoint:
+ parts.append('$')
+ return ''.join(parts), converters
+
+
+class RoutePattern(CheckURLMixin):
+ regex = LocaleRegexDescriptor('_route')
+
+ def __init__(self, route, name=None, is_endpoint=False):
+ self._route = route
+ self._regex_dict = {}
+ self._is_endpoint = is_endpoint
+ self.name = name
+ self.converters = _route_to_regex(str(route), is_endpoint)[1]
+
+ def match(self, path):
+ match = self.regex.search(path)
+ if match:
+ # RoutePattern doesn't allow non-named groups so args are ignored.
+ kwargs = match.groupdict()
+ for key, value in kwargs.items():
+ converter = self.converters[key]
+ try:
+ kwargs[key] = converter.to_python(value)
+ except ValueError:
+ return None
+ return path[match.end():], (), kwargs
+ return None
+
+ def check(self):
+ return self._check_pattern_startswith_slash()
+
+ def _compile(self, route):
+ return re.compile(_route_to_regex(route, self._is_endpoint)[0])
+
+ def __str__(self):
+ return self._route
+
+
+class LocalePrefixPattern:
+ def __init__(self, prefix_default_language=True):
+ self.prefix_default_language = prefix_default_language
+ self.converters = {}
+
+ @property
+ def regex(self):
+ # This is only used by reverse() and cached in _reverse_dict.
+ return re.compile(self.language_prefix)
+
+ @property
+ def language_prefix(self):
+ language_code = get_language() or settings.LANGUAGE_CODE
+ if language_code == settings.LANGUAGE_CODE and not self.prefix_default_language:
+ return ''
+ else:
+ return '%s/' % language_code
+
+ def match(self, path):
+ language_prefix = self.language_prefix
+ if path.startswith(language_prefix):
+ return path[len(language_prefix):], (), {}
+ return None
+
+ def check(self):
+ return []
+
+ def describe(self):
+ return "'{}'".format(self)
+
+ def __str__(self):
+ return self.language_prefix
+
+
+class URLPattern:
+ def __init__(self, pattern, callback, default_args=None, name=None):
+ self.pattern = pattern
self.callback = callback # the view
self.default_args = default_args or {}
self.name = name
def __repr__(self):
- return '<%s %s %s>' % (self.__class__.__name__, self.name, self.regex.pattern)
+ return '<%s %s>' % (self.__class__.__name__, self.pattern.describe())
def check(self):
warnings = self._check_pattern_name()
- if not warnings:
- warnings = self._check_pattern_startswith_slash()
+ warnings.extend(self.pattern.check())
return warnings
def _check_pattern_name(self):
"""
Check that the pattern name does not contain a colon.
"""
- if self.name is not None and ":" in self.name:
+ if self.pattern.name is not None and ":" in self.pattern.name:
warning = Warning(
"Your URL pattern {} has a name including a ':'. Remove the colon, to "
- "avoid ambiguous namespace references.".format(self.describe()),
+ "avoid ambiguous namespace references.".format(self.pattern.describe()),
id="urls.W003",
)
return [warning]
@@ -183,16 +329,12 @@ class RegexURLPattern(LocaleRegexProvider):
return []
def resolve(self, path):
- match = self.regex.search(path)
+ match = self.pattern.match(path)
if match:
- # If there are any named groups, use those as kwargs, ignoring
- # non-named groups. Otherwise, pass all non-named arguments as
- # positional arguments.
- kwargs = match.groupdict()
- args = () if kwargs else match.groups()
- # In both cases, pass any extra_kwargs as **kwargs.
+ new_path, args, kwargs = match
+ # Pass any extra_kwargs as **kwargs.
kwargs.update(self.default_args)
- return ResolverMatch(self.callback, args, kwargs, self.name)
+ return ResolverMatch(self.callback, args, kwargs, self.pattern.name)
@cached_property
def lookup_str(self):
@@ -210,9 +352,9 @@ class RegexURLPattern(LocaleRegexProvider):
return callback.__module__ + "." + callback.__qualname__
-class RegexURLResolver(LocaleRegexProvider):
- def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
- LocaleRegexProvider.__init__(self, regex)
+class URLResolver:
+ def __init__(self, pattern, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
+ self.pattern = pattern
# urlconf_name is the dotted Python path to the module defining
# urlpatterns. It may also be an object with an urlpatterns attribute
# or urlpatterns itself.
@@ -238,33 +380,17 @@ class RegexURLResolver(LocaleRegexProvider):
urlconf_repr = repr(self.urlconf_name)
return '<%s %s (%s:%s) %s>' % (
self.__class__.__name__, urlconf_repr, self.app_name,
- self.namespace, self.regex.pattern,
+ self.namespace, self.pattern.describe(),
)
def check(self):
- warnings = self._check_include_trailing_dollar()
+ warnings = []
for pattern in self.url_patterns:
warnings.extend(check_resolver(pattern))
if not warnings:
- warnings = self._check_pattern_startswith_slash()
+ warnings = self.pattern.check()
return warnings
- def _check_include_trailing_dollar(self):
- """
- Check that include is not used with a regex ending with a dollar.
- """
- regex_pattern = self.regex.pattern
- if regex_pattern.endswith('$') and not regex_pattern.endswith(r'\$'):
- warning = Warning(
- "Your URL pattern {} uses include with a regex ending with a '$'. "
- "Remove the dollar from the regex to avoid problems including "
- "URLs.".format(self.describe()),
- id="urls.W001",
- )
- return [warning]
- else:
- return []
-
def _populate(self):
# Short-circuit if called recursively in this thread to prevent
# infinite recursion. Concurrent threads may call this at the same
@@ -277,47 +403,52 @@ class RegexURLResolver(LocaleRegexProvider):
namespaces = {}
apps = {}
language_code = get_language()
- for pattern in reversed(self.url_patterns):
- if isinstance(pattern, RegexURLPattern):
- self._callback_strs.add(pattern.lookup_str)
- p_pattern = pattern.regex.pattern
- if p_pattern.startswith('^'):
- p_pattern = p_pattern[1:]
- if isinstance(pattern, RegexURLResolver):
- if pattern.namespace:
- namespaces[pattern.namespace] = (p_pattern, pattern)
- if pattern.app_name:
- apps.setdefault(pattern.app_name, []).append(pattern.namespace)
- else:
- parent_pat = pattern.regex.pattern
- for name in pattern.reverse_dict:
- for matches, pat, defaults in pattern.reverse_dict.getlist(name):
- new_matches = normalize(parent_pat + pat)
- lookups.appendlist(
- name,
- (
- new_matches,
- p_pattern + pat,
- dict(defaults, **pattern.default_kwargs),
+ try:
+ for url_pattern in reversed(self.url_patterns):
+ p_pattern = url_pattern.pattern.regex.pattern
+ if p_pattern.startswith('^'):
+ p_pattern = p_pattern[1:]
+ if isinstance(url_pattern, URLPattern):
+ self._callback_strs.add(url_pattern.lookup_str)
+ bits = normalize(url_pattern.pattern.regex.pattern)
+ lookups.appendlist(
+ url_pattern.callback,
+ (bits, p_pattern, url_pattern.default_args, url_pattern.pattern.converters)
+ )
+ if url_pattern.name is not None:
+ lookups.appendlist(
+ url_pattern.name,
+ (bits, p_pattern, url_pattern.default_args, url_pattern.pattern.converters)
+ )
+ else: # url_pattern is a URLResolver.
+ url_pattern._populate()
+ if url_pattern.app_name:
+ apps.setdefault(url_pattern.app_name, []).append(url_pattern.namespace)
+ namespaces[url_pattern.namespace] = (p_pattern, url_pattern)
+ else:
+ for name in url_pattern.reverse_dict:
+ for matches, pat, defaults, converters in url_pattern.reverse_dict.getlist(name):
+ new_matches = normalize(p_pattern + pat)
+ lookups.appendlist(
+ name,
+ (
+ new_matches,
+ p_pattern + pat,
+ dict(defaults, **url_pattern.default_kwargs),
+ dict(self.pattern.converters, **converters)
+ )
)
- )
- for namespace, (prefix, sub_pattern) in pattern.namespace_dict.items():
- namespaces[namespace] = (p_pattern + prefix, sub_pattern)
- for app_name, namespace_list in pattern.app_dict.items():
- apps.setdefault(app_name, []).extend(namespace_list)
- if not getattr(pattern._local, 'populating', False):
- pattern._populate()
- self._callback_strs.update(pattern._callback_strs)
- else:
- bits = normalize(p_pattern)
- lookups.appendlist(pattern.callback, (bits, p_pattern, pattern.default_args))
- if pattern.name is not None:
- lookups.appendlist(pattern.name, (bits, p_pattern, pattern.default_args))
- self._reverse_dict[language_code] = lookups
- self._namespace_dict[language_code] = namespaces
- self._app_dict[language_code] = apps
- self._populated = True
- self._local.populating = False
+ for namespace, (prefix, sub_pattern) in url_pattern.namespace_dict.items():
+ namespaces[namespace] = (p_pattern + prefix, sub_pattern)
+ for app_name, namespace_list in url_pattern.app_dict.items():
+ apps.setdefault(app_name, []).extend(namespace_list)
+ self._callback_strs.update(url_pattern._callback_strs)
+ self._namespace_dict[language_code] = namespaces
+ self._app_dict[language_code] = apps
+ self._reverse_dict[language_code] = lookups
+ self._populated = True
+ finally:
+ self._local.populating = False
@property
def reverse_dict(self):
@@ -348,9 +479,9 @@ class RegexURLResolver(LocaleRegexProvider):
def resolve(self, path):
path = str(path) # path may be a reverse_lazy object
tried = []
- match = self.regex.search(path)
+ match = self.pattern.match(path)
if match:
- new_path = path[match.end():]
+ new_path, args, kwargs = match
for pattern in self.url_patterns:
try:
sub_match = pattern.resolve(new_path)
@@ -363,15 +494,14 @@ class RegexURLResolver(LocaleRegexProvider):
else:
if sub_match:
# Merge captured arguments in match with submatch
- sub_match_dict = dict(match.groupdict(), **self.default_kwargs)
+ sub_match_dict = dict(kwargs, **self.default_kwargs)
+ # Update the sub_match_dict with the kwargs from the sub_match.
sub_match_dict.update(sub_match.kwargs)
-
# If there are *any* named groups, ignore all non-named groups.
# Otherwise, pass all non-named arguments as positional arguments.
sub_match_args = sub_match.args
if not sub_match_dict:
- sub_match_args = match.groups() + sub_match.args
-
+ sub_match_args = args + sub_match.args
return ResolverMatch(
sub_match.func,
sub_match_args,
@@ -421,20 +551,18 @@ class RegexURLResolver(LocaleRegexProvider):
def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs):
if args and kwargs:
raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
- text_args = [str(v) for v in args]
- text_kwargs = {k: str(v) for (k, v) in kwargs.items()}
if not self._populated:
self._populate()
possibilities = self.reverse_dict.getlist(lookup_view)
- for possibility, pattern, defaults in possibilities:
+ for possibility, pattern, defaults, converters in possibilities:
for result, params in possibility:
if args:
if len(args) != len(params):
continue
- candidate_subs = dict(zip(params, text_args))
+ candidate_subs = dict(zip(params, args))
else:
if set(kwargs).symmetric_difference(params).difference(defaults):
continue
@@ -445,16 +573,23 @@ class RegexURLResolver(LocaleRegexProvider):
break
if not matches:
continue
- candidate_subs = text_kwargs
+ candidate_subs = kwargs
+ # Convert the candidate subs to text using Converter.to_url().
+ text_candidate_subs = {}
+ for k, v in candidate_subs.items():
+ if k in converters:
+ text_candidate_subs[k] = converters[k].to_url(v)
+ else:
+ text_candidate_subs[k] = str(v)
# WSGI provides decoded URLs, without %xx escapes, and the URL
# resolver operates on such URLs. First substitute arguments
# without quoting to build a decoded URL and look for a match.
# Then, if we have a match, redo the substitution with quoted
# arguments in order to return a properly encoded URL.
candidate_pat = _prefix.replace('%', '%%') + result
- if re.search('^%s%s' % (re.escape(_prefix), pattern), candidate_pat % candidate_subs):
+ if re.search('^%s%s' % (re.escape(_prefix), pattern), candidate_pat % text_candidate_subs):
# safe characters from `pchar` definition of RFC 3986
- url = quote(candidate_pat % candidate_subs, safe=RFC3986_SUBDELIMS + '/~:@')
+ url = quote(candidate_pat % text_candidate_subs, safe=RFC3986_SUBDELIMS + '/~:@')
# Don't allow construction of scheme relative urls.
if url.startswith('//'):
url = '/%%2F%s' % url[2:]
@@ -468,7 +603,7 @@ class RegexURLResolver(LocaleRegexProvider):
else:
lookup_view_s = lookup_view
- patterns = [pattern for (possibility, pattern, defaults) in possibilities]
+ patterns = [pattern for (_, pattern, _, _) in possibilities]
if patterns:
if args:
arg_msg = "arguments '%s'" % (args,)
@@ -486,29 +621,3 @@ class RegexURLResolver(LocaleRegexProvider):
"a valid view function or pattern name." % {'view': lookup_view_s}
)
raise NoReverseMatch(msg)
-
-
-class LocaleRegexURLResolver(RegexURLResolver):
- """
- A URL resolver that always matches the active language code as URL prefix.
-
- Rather than taking a regex argument, we just override the ``regex``
- function to always return the active language-code as regex.
- """
- def __init__(
- self, urlconf_name, default_kwargs=None, app_name=None, namespace=None,
- prefix_default_language=True,
- ):
- super().__init__(None, urlconf_name, default_kwargs, app_name, namespace)
- self.prefix_default_language = prefix_default_language
-
- @property
- def regex(self):
- language_code = get_language() or settings.LANGUAGE_CODE
- if language_code not in self._regex_dict:
- if language_code == settings.LANGUAGE_CODE and not self.prefix_default_language:
- regex_string = ''
- else:
- regex_string = '^%s/' % language_code
- self._regex_dict[language_code] = re.compile(regex_string)
- return self._regex_dict[language_code]
diff --git a/django/views/generic/dates.py b/django/views/generic/dates.py
index 5e0247474d..2aedf3aacd 100644
--- a/django/views/generic/dates.py
+++ b/django/views/generic/dates.py
@@ -612,7 +612,7 @@ def _date_from_string(year, year_format, month='', month_format='', day='', day_
(only year is mandatory). Raise a 404 for an invalid date.
"""
format = year_format + delim + month_format + delim + day_format
- datestr = year + delim + month + delim + day
+ datestr = str(year) + delim + str(month) + delim + str(day)
try:
return datetime.datetime.strptime(datestr, format).date()
except ValueError:
diff --git a/django/views/templates/technical_404.html b/django/views/templates/technical_404.html
index ec518f658e..0bf5fd3cd0 100644
--- a/django/views/templates/technical_404.html
+++ b/django/views/templates/technical_404.html
@@ -52,7 +52,7 @@
{% for pattern in urlpatterns %}
<li>
{% for pat in pattern %}
- {{ pat.regex.pattern }}
+ {{ pat.pattern }}
{% if forloop.last and pat.name %}[name='{{ pat.name }}']{% endif %}
{% endfor %}
</li>