Ticket #16774: 16774.diff

File 16774.diff, 18.2 KB (added by julien, 4 years ago)
  • django/core/handlers/base.py

    diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py
    index 3606e18..ce51d8a 100644
    a b class BaseHandler(object): 
    9797                        urlresolvers.set_urlconf(urlconf)
    9898                        resolver = urlresolvers.RegexURLResolver(r'^/', urlconf)
    9999
    100                     callback, callback_args, callback_kwargs = resolver.resolve(
    101                             request.path_info)
     100                    for candidate in resolver.backtracking_resolve(request.path_info):
     101                        callback, callback_args, callback_kwargs = candidate.func, candidate.args, candidate.kwargs
    102102
    103                     # Apply view middleware
    104                     for middleware_method in self._view_middleware:
    105                         response = middleware_method(request, callback, callback_args, callback_kwargs)
    106                         if response:
    107                             break
    108 
    109                 if response is None:
    110                     try:
    111                         response = callback(request, *callback_args, **callback_kwargs)
    112                     except Exception, e:
    113                         # If the view raised an exception, run it through exception
    114                         # middleware, and if the exception middleware returns a
    115                         # response, use that. Otherwise, reraise the exception.
    116                         for middleware_method in self._exception_middleware:
    117                             response = middleware_method(request, e)
     103                        # Apply view middleware
     104                        for middleware_method in self._view_middleware:
     105                            response = middleware_method(request, callback, callback_args, callback_kwargs)
    118106                            if response:
    119107                                break
     108
    120109                        if response is None:
    121                             raise
     110                            try:
     111                                response = callback(request, *callback_args, **callback_kwargs)
     112                                break
     113                            except urlresolvers.ContinueResolving:
     114                                # If the URL resolver candidate raised a ContinueResolving,
     115                                # allow the URL resolver to pick up where it left off
     116                                continue
     117                            except Exception, e:
     118                                # If the view raised an exception, run it through exception
     119                                # middleware, and if the exception middleware returns a
     120                                # response, use that. Otherwise, reraise the exception.
     121                                for middleware_method in self._exception_middleware:
     122                                    response = middleware_method(request, e)
     123                                    if response:
     124                                        break
     125                                if response is None:
     126                                    raise
     127
     128                        # We need to ensure that we break out of the URL resolver
     129                        # search loop, since we found a URL resolver match
     130                        # but the matched view did not raise a ContinueResolving exception
     131                        # so we want to terminate this search now
     132                        break
    122133
    123134                # Complain if the view returned None (a common error).
    124135                if response is None:
  • django/core/urlresolvers.py

    diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py
    index a253319..d2fd50d 100644
    a b class ResolverMatch(object): 
    6868        return "ResolverMatch(func=%s, args=%s, kwargs=%s, url_name='%s', app_name='%s', namespace='%s')" % (
    6969            self.func, self.args, self.kwargs, self.url_name, self.app_name, self.namespace)
    7070
     71class ResolverMatches(object):
     72    def __init__(self, resolver):
     73        self.resolver = resolver
     74        self.__in_iteration = False
     75
     76    def __iter__(self):
     77        return self
     78
     79    def __repr__(self):
     80        return self.candidate.__repr__()
     81
     82    def next(self, peek=False):
     83        # If this is the first time accessing our iterator, or we have
     84        # already started iteration, fetch and store the next candidate,
     85        # or we want to keep in on the current candidate
     86        if not hasattr(self, '_candidate') or (self.__in_iteration and not peek):
     87            self._candidate = self.resolver.next()
     88
     89        # Functions like __nonzero__ should not trigger the start of
     90        # iteration over the resolver generator. They need to know
     91        # if the generator is empty or not, but their peeking should not
     92        # remove an item from the generator for those that attempt to
     93        # iterate over the generator at a later point.
     94        if not peek:
     95            self.__in_iteration = True
     96        return self._candidate
     97
     98    def candidate(self):
     99        return self.next(peek=True)
     100    candidate = property(candidate)
     101
     102    def __nonzero__(self):
     103        try:
     104            self.next(peek=True)
     105        except Resolver404:
     106            return False
     107        return True
     108
    71109class Resolver404(Http404):
    72110    pass
    73111
     112class ContinueResolving(Resolver404):
     113    pass
     114
    74115class NoReverseMatch(Exception):
    75116    # Don't make this raise an error when used in a template.
    76117    silent_variable_failure = True
    class RegexURLResolver(LocaleRegexProvider): 
    291332        return self._app_dict[language_code]
    292333
    293334    def resolve(self, path):
    294         tried = []
     335        return self.backtracking_resolve(path).next()
     336
     337    def backtracking_resolve(self, path, tried=None):
     338        return ResolverMatches(self.__backtracking_resolve(path, tried))
     339
     340    def __backtracking_resolve(self, path, tried=None):
     341        if tried is None:
     342            tried = []
     343
    295344        match = self.regex.search(path)
    296345        if match:
    297346            new_path = path[match.end():]
    298347            for pattern in self.url_patterns:
     348                sub_tried = []
     349                sub_matches = []
    299350                try:
    300                     sub_match = pattern.resolve(new_path)
     351                    if isinstance(pattern, RegexURLResolver):
     352                        sub_matches = pattern.backtracking_resolve(new_path, sub_tried)
     353                    else:
     354                        sub_match = pattern.resolve(new_path)
     355                        if sub_match:
     356                            sub_matches = [sub_match]
    301357                except Resolver404, e:
    302358                    sub_tried = e.args[0].get('tried')
    303359                    if sub_tried is not None:
    class RegexURLResolver(LocaleRegexProvider): 
    305361                    else:
    306362                        tried.append([pattern])
    307363                else:
    308                     if sub_match:
    309                         sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()])
    310                         sub_match_dict.update(self.default_kwargs)
    311                         for k, v in sub_match.kwargs.iteritems():
    312                             sub_match_dict[smart_str(k)] = v
    313                         return ResolverMatch(sub_match.func, sub_match.args, sub_match_dict, sub_match.url_name, self.app_name or sub_match.app_name, [self.namespace] + sub_match.namespaces)
    314                     tried.append([pattern])
     364                    if sub_matches:
     365                        for sub_match in sub_matches:
     366                            sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()])
     367                            sub_match_dict.update(self.default_kwargs)
     368                            for k, v in sub_match.kwargs.iteritems():
     369                                sub_match_dict[smart_str(k)] = v
     370                            yield ResolverMatch(
     371                                    sub_match.func,
     372                                    sub_match.args,
     373                                    sub_match_dict,
     374                                    sub_match.url_name,
     375                                    self.app_name or sub_match.app_name,
     376                                    [self.namespace] + sub_match.namespaces,
     377                                    )
     378                    if sub_tried:
     379                        tried.extend([[pattern] + t for t in sub_tried])
     380                    else:
     381                        tried.append([pattern])
    315382            raise Resolver404({'tried': tried, 'path': new_path})
    316383        raise Resolver404({'path' : path})
    317384
    def resolve(path, urlconf=None): 
    415482        urlconf = get_urlconf()
    416483    return get_resolver(urlconf).resolve(path)
    417484
     485def backtracking_resolve(path, urlconf=None):
     486    if urlconf is None:
     487        urlconf = get_urlconf()
     488    return get_resolver(urlconf).backtracking_resolve(path)
     489
    418490def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None):
    419491    if urlconf is None:
    420492        urlconf = get_urlconf()
  • docs/topics/http/urls.txt

    diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt
    index 5c9711c..7a4ad98 100644
    a b algorithm the system follows to determine which Python code to execute: 
    5151   ``urlpatterns``. This should be a Python list, in the format returned by
    5252   the function :func:`django.conf.urls.patterns`.
    5353
    54 3. Django runs through each URL pattern, in order, and stops at the first
    55    one that matches the requested URL.
    56 
    57 4. Once one of the regexes matches, Django imports and calls the given
    58    view, which is a simple Python function. The view gets passed an
    59    :class:`~django.http.HttpRequest` as its first argument and any values
    60    captured in the regex as remaining arguments.
     543. Django runs through each URL pattern, in order, and for each matched pattern
     55   calls the corresponding view. If a view raises a
     56   :class:`~django.core.urlresolvers.ContinueResolving` exception it will
     57   continue calling the next matched pattern until it runs out of matches or a
     58   view does not raise an :class:`~django.core.urlresolvers.ContinueResolving`
     59   exception, in which case it will return the matched views response.
     60
     614. The view called by a matched pattern of the URL resolver is a simple Python
     62   function that gets passed an :class:`~django.http.HttpRequest` as its first
     63   argument and any values captured in the regex as remaining arguments. As
     64   mentioned above, a view can raise a
     65   :class:`~django.core.urlresolvers.ContinueResolving` exception to allow the
     66   URL resolver to continue searching for potential URL pattern matches.
    6167
    62685. If no regex matches, or if an exception is raised during any
    6369   point in this process, Django invokes an appropriate
  • tests/regressiontests/test_client_regress/models.py

    diff --git a/tests/regressiontests/test_client_regress/models.py b/tests/regressiontests/test_client_regress/models.py
    index b1e5bc4..d369a21 100644
    a b class UnicodePayloadTests(TestCase): 
    899899                                    content_type="application/json; charset=koi8-r")
    900900        self.assertEqual(response.content, json.encode('koi8-r'))
    901901
     902class BacktrackingResolveTests(TestCase):
     903    def test_capture(self):
     904        response = self.client.get("/test_client_regress/capture-foo")
     905        self.assertEqual(response.content, "Success for backtracking_resolve_dynamic_capture: capture-foo")
     906
     907        response = self.client.get("/test_client_regress/bar-capture")
     908        self.assertEqual(response.content, "Success for backtracking_resolve_dynamic_capture: bar-capture")
     909
     910    def test_redirect(self):
     911        response = self.client.get("/test_client_regress/redirect-bar")
     912        self.assertRedirects(response, "/test_client_regress/arg_view/test/", status_code=302, target_status_code=200)
     913
     914    def test_failed_capture(self):
     915        response = self.client.get("/test_client_regress/redirect-foo")
     916        self.assertEqual(response.status_code, 404)
     917
    902918class DummyFile(object):
    903919    def __init__(self, filename):
    904920        self.name = filename
  • tests/regressiontests/test_client_regress/urls.py

    diff --git a/tests/regressiontests/test_client_regress/urls.py b/tests/regressiontests/test_client_regress/urls.py
    index 5590c23..996fe76 100644
    a b from . import views 
    77
    88
    99urlpatterns = patterns('',
     10    (r'^(?P<data>.*)$', views.backtracking_resolve_dynamic_capture),
    1011    (r'^no_template_view/$', views.no_template_view),
    1112    (r'^staff_only/$', views.staff_only_view),
    1213    (r'^get_view/$', views.get_view),
    urlpatterns = patterns('', 
    2021    (r'^redirect_to_non_existent_view/$', RedirectView.as_view(url='/test_client_regress/non_existent_view/')),
    2122    (r'^redirect_to_non_existent_view2/$', RedirectView.as_view(url='/test_client_regress/redirect_to_non_existent_view/')),
    2223    (r'^redirect_to_self/$', RedirectView.as_view(url='/test_client_regress/redirect_to_self/')),
     24    (r'^(?P<data>.*)$', views.backtracking_resolve_dynamic_redirect),
    2325    (r'^circular_redirect_1/$', RedirectView.as_view(url='/test_client_regress/circular_redirect_2/')),
    2426    (r'^circular_redirect_2/$', RedirectView.as_view(url='/test_client_regress/circular_redirect_3/')),
    2527    (r'^circular_redirect_3/$', RedirectView.as_view(url='/test_client_regress/circular_redirect_1/')),
  • tests/regressiontests/test_client_regress/views.py

    diff --git a/tests/regressiontests/test_client_regress/views.py b/tests/regressiontests/test_client_regress/views.py
    index b398293..6260305 100644
    a b  
    11from django.conf import settings
    22from django.contrib.auth.decorators import login_required
     3from django.core.urlresolvers import ContinueResolving, reverse
    34from django.http import HttpResponse, HttpResponseRedirect
    45from django.core.exceptions import SuspiciousOperation
    56from django.shortcuts import render_to_response
    from django.core.serializers.json import DjangoJSONEncoder 
    910from django.test.client import CONTENT_TYPE_RE
    1011from django.template import RequestContext
    1112
     13
     14def backtracking_resolve_dynamic_capture(request, data):
     15    if data in ('capture-foo', 'bar-capture',):
     16        return HttpResponse("Success for backtracking_resolve_dynamic_capture: %s" % data)
     17    raise ContinueResolving
     18
     19def backtracking_resolve_dynamic_redirect(request, data):
     20    if data in ('redirect-bar', 'baz-redirect',):
     21        return HttpResponseRedirect(reverse('arg_view', kwargs={'name': 'test'}))
     22    raise ContinueResolving
     23
    1224def no_template_view(request):
    1325    "A simple view that expects a GET request, and returns a rendered template"
    1426    return HttpResponse("No template used. Sample content: twice once twice. Content ends.")
  • new file tests/regressiontests/urlpatterns_reverse/backtracking_urls.py

    diff --git a/tests/regressiontests/urlpatterns_reverse/backtracking_urls.py b/tests/regressiontests/urlpatterns_reverse/backtracking_urls.py
    new file mode 100644
    index 0000000..9dc468c
    - +  
     1from django.conf.urls.defaults import *
     2from views import continue_resolving_view, backtracking_view
     3
     4urlpatterns = patterns('',
     5    url(r'', continue_resolving_view, name='continue_resolving_view'),
     6    url(r'^backtrack/$', backtracking_view, name='backtracking_view'),
     7)
     8 No newline at end of file
  • tests/regressiontests/urlpatterns_reverse/tests.py

    diff --git a/tests/regressiontests/urlpatterns_reverse/tests.py b/tests/regressiontests/urlpatterns_reverse/tests.py
    index 307d8da..bbb1cf4 100644
    a b from __future__ import absolute_import 
    55
    66from django.conf import settings
    77from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
    8 from django.core.urlresolvers import (reverse, resolve, NoReverseMatch,
    9     Resolver404, ResolverMatch, RegexURLResolver, RegexURLPattern)
     8from django.core.urlresolvers import (backtracking_resolve, reverse, resolve,
     9    NoReverseMatch, Resolver404, ResolverMatch, RegexURLResolver,
     10    RegexURLPattern)
    1011from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
    1112from django.shortcuts import redirect
    1213from django.test import TestCase
    1314from django.utils import unittest
    1415from django.contrib.auth.models import User
    1516
    16 from . import urlconf_outer, urlconf_inner, middleware, views
     17from . import urlconf_outer, middleware, views
    1718
    1819
    1920resolve_test_data = (
    class URLPatternReverse(TestCase): 
    164165        # Reversing None should raise an error, not return the last un-named view.
    165166        self.assertRaises(NoReverseMatch, reverse, None)
    166167
     168class BacktrackingUrlTests(TestCase):
     169    urls = 'regressiontests.urlpatterns_reverse.backtracking_urls'
     170
     171    def test_backtracking_normal_reverse(self):
     172        self.assertEqual(reverse('backtracking_view'), '/backtrack/')
     173
     174    def test_backtracking_normal_resolve(self):
     175        r = backtracking_resolve('/backtrack/')
     176        first = r.next()
     177        self.assertEqual(first.func.__name__, 'continue_resolving_view')
     178        second = r.next()
     179        self.assertEqual(second.func.__name__, 'backtracking_view')
     180        self.assertRaises(Resolver404, r.next)
     181
     182        response = self.client.get('/backtrack/')
     183        self.assertEqual(response.status_code, 200)
     184        self.assertEqual(response.content, 'backtracked ok')
     185
    167186class ResolverTests(unittest.TestCase):
    168187    def test_non_regex(self):
    169188        """
  • tests/regressiontests/urlpatterns_reverse/views.py

    diff --git a/tests/regressiontests/urlpatterns_reverse/views.py b/tests/regressiontests/urlpatterns_reverse/views.py
    index f631acf..2bb4c45 100644
    a b  
     1from django.core.urlresolvers import ContinueResolving
    12from django.http import HttpResponse
    23from django.views.generic import RedirectView
    34from django.core.urlresolvers import reverse_lazy
    def kwargs_view(request, arg1=1, arg2=2): 
    1314def absolute_kwargs_view(request, arg1=1, arg2=2):
    1415    return HttpResponse('')
    1516
     17def continue_resolving_view(request, *args, **kwargs):
     18    raise ContinueResolving
     19
     20def backtracking_view(request, *args, **kwargs):
     21    return HttpResponse('backtracked ok')
     22
    1623def defaults_view(request, arg1, arg2):
    1724    pass
    1825
Back to Top