Ticket #16774: backtracking_resolver.diff

File backtracking_resolver.diff, 18.0 KB (added by Nowell Strite, 13 years ago)

Implemented jacobm's suggestion and separated the backtracking resolver exception off into it's own subclass of Resolver404 exception to provide a cleaner API as well as ensure no possible backwards incompatibility.

  • django/core/handlers/base.py

    diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py
    index a6c8044..6e883e0 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 709a1fe..fdb9013 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 4bb3b78..407daa5 100644
    a b algorithm the system follows to determine which Python code to execute:  
    5252       ``urlpatterns``. This should be a Python list, in the format returned by
    5353       the function :func:`django.conf.urls.defaults.patterns`.
    5454
    55     3. Django runs through each URL pattern, in order, and stops at the first
    56        one that matches the requested URL.
    57 
    58     4. Once one of the regexes matches, Django imports and calls the given
    59        view, which is a simple Python function. The view gets passed an
    60        :class:`~django.http.HttpRequest` as its first argument and any values
    61        captured in the regex as remaining arguments.
     55    3. Django runs through each URL pattern, in order, and for each matched pattern
     56       calls the corresponding view. If a view raises a
     57       :class:`~django.core.urlresolvers.ContinueResolving` exception it will
     58       continue calling the next matched pattern until it runs out of matches or a
     59       view does not raise an :class:`~django.core.urlresolvers.ContinueResolving`
     60       exception, in which case it will return the matched views response.
     61   
     62    4. The view called by a matched pattern of the URL resolver is a simple Python
     63       function that gets passed an :class:`~django.http.HttpRequest` as its first
     64       argument and any values captured in the regex as remaining arguments. As
     65       mentioned above, a view can raise a
     66       :class:`~django.core.urlresolvers.ContinueResolving` exception to allow the
     67       URL resolver to continue searching for potential URL pattern matches.
    6268
    6369Example
    6470=======
  • 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 18ffffd..3e606f8 100644
    a b class UnicodePayloadTests(TestCase):  
    866866                                    content_type="application/json; charset=koi8-r")
    867867        self.assertEqual(response.content, json.encode('koi8-r'))
    868868
     869class BacktrackingResolveTests(TestCase):
     870    def test_capture(self):
     871        response = self.client.get("/test_client_regress/capture-foo")
     872        self.assertEqual(response.content, "Success for backtracking_resolve_dynamic_capture: capture-foo")
     873
     874        response = self.client.get("/test_client_regress/bar-capture")
     875        self.assertEqual(response.content, "Success for backtracking_resolve_dynamic_capture: bar-capture")
     876
     877    def test_redirect(self):
     878        response = self.client.get("/test_client_regress/redirect-bar")
     879        self.assertRedirects(response, "/test_client_regress/arg_view/test/", status_code=302, target_status_code=200)
     880
     881    def test_failed_capture(self):
     882        response = self.client.get("/test_client_regress/redirect-foo")
     883        self.assertEqual(response.status_code, 404)
     884
    869885class DummyFile(object):
    870886    def __init__(self, filename):
    871887        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 454f35b..7ff6d99 100644
    a b from django.views.generic import RedirectView  
    33import views
    44
    55urlpatterns = patterns('',
     6    (r'^(?P<data>.*)$', views.backtracking_resolve_dynamic_capture),
    67    (r'^no_template_view/$', views.no_template_view),
    78    (r'^staff_only/$', views.staff_only_view),
    89    (r'^get_view/$', views.get_view),
    urlpatterns = patterns('',  
    1617    (r'^redirect_to_non_existent_view/$', RedirectView.as_view(url='/test_client_regress/non_existent_view/')),
    1718    (r'^redirect_to_non_existent_view2/$', RedirectView.as_view(url='/test_client_regress/redirect_to_non_existent_view/')),
    1819    (r'^redirect_to_self/$', RedirectView.as_view(url='/test_client_regress/redirect_to_self/')),
     20    (r'^(?P<data>.*)$', views.backtracking_resolve_dynamic_redirect),
    1921    (r'^circular_redirect_1/$', RedirectView.as_view(url='/test_client_regress/circular_redirect_2/')),
    2022    (r'^circular_redirect_2/$', RedirectView.as_view(url='/test_client_regress/circular_redirect_3/')),
    2123    (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..1510f5a
    - +  
     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)
  • tests/regressiontests/urlpatterns_reverse/tests.py

    diff --git a/tests/regressiontests/urlpatterns_reverse/tests.py b/tests/regressiontests/urlpatterns_reverse/tests.py
    index 4b656e4..218c7f3 100644
    a b Unit tests for reverse URL lookups.  
    33"""
    44from django.conf import settings
    55from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
    6 from django.core.urlresolvers import (reverse, resolve, NoReverseMatch,
    7     Resolver404, ResolverMatch, RegexURLResolver, RegexURLPattern)
     6from django.core.urlresolvers import (backtracking_resolve, reverse, resolve,
     7    NoReverseMatch, Resolver404, ResolverMatch, RegexURLResolver, RegexURLPattern)
    88from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
    99from django.shortcuts import redirect
    1010from django.test import TestCase
    class URLPatternReverse(TestCase):  
    164164        # Reversing None should raise an error, not return the last un-named view.
    165165        self.assertRaises(NoReverseMatch, reverse, None)
    166166
     167class BacktrackingUrlTests(TestCase):
     168    urls = 'regressiontests.urlpatterns_reverse.backtracking_urls'
     169
     170    def test_backtracking_normal_reverse(self):
     171        self.assertEqual(reverse('backtracking_view'), '/backtrack/')
     172
     173    def test_backtracking_normal_resolve(self):
     174        r = backtracking_resolve('/backtrack/')
     175        first = r.next()
     176        self.assertEqual(first.func.__name__, 'continue_resolving_view')
     177        second = r.next()
     178        self.assertEqual(second.func.__name__, 'backtracking_view')
     179        self.assertRaises(Resolver404, r.next)
     180
     181        response = self.client.get('/backtrack/')
     182        self.assertEqual(response.status_code, 200)
     183        self.assertEqual(response.content, 'backtracked ok')
     184
    167185class ResolverTests(unittest.TestCase):
    168186    def test_non_regex(self):
    169187        """
  • 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