diff options
Diffstat (limited to 'django')
| -rw-r--r-- | django/conf/project_template/project_name/urls.py-tpl | 12 | ||||
| -rw-r--r-- | django/conf/urls/__init__.py | 11 | ||||
| -rw-r--r-- | django/conf/urls/i18n.py | 16 | ||||
| -rw-r--r-- | django/conf/urls/static.py | 4 | ||||
| -rw-r--r-- | django/contrib/admin/options.py | 19 | ||||
| -rw-r--r-- | django/contrib/admin/sites.py | 34 | ||||
| -rw-r--r-- | django/contrib/admindocs/urls.py | 56 | ||||
| -rw-r--r-- | django/contrib/admindocs/views.py | 5 | ||||
| -rw-r--r-- | django/contrib/auth/admin.py | 7 | ||||
| -rw-r--r-- | django/contrib/auth/urls.py | 19 | ||||
| -rw-r--r-- | django/contrib/flatpages/urls.py | 4 | ||||
| -rw-r--r-- | django/core/checks/urls.py | 4 | ||||
| -rw-r--r-- | django/template/defaulttags.py | 8 | ||||
| -rw-r--r-- | django/urls/__init__.py | 20 | ||||
| -rw-r--r-- | django/urls/conf.py | 31 | ||||
| -rw-r--r-- | django/urls/converters.py | 70 | ||||
| -rw-r--r-- | django/urls/resolvers.py | 415 | ||||
| -rw-r--r-- | django/views/generic/dates.py | 2 | ||||
| -rw-r--r-- | django/views/templates/technical_404.html | 2 |
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> |
