Ticket #16774: backtracking_resolver.diff
File backtracking_resolver.diff, 18.0 KB (added by , 13 years ago) |
---|
-
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): 97 97 urlresolvers.set_urlconf(urlconf) 98 98 resolver = urlresolvers.RegexURLResolver(r'^/', urlconf) 99 99 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 102 102 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) 118 106 if response: 119 107 break 108 120 109 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 122 133 123 134 # Complain if the view returned None (a common error). 124 135 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): 68 68 return "ResolverMatch(func=%s, args=%s, kwargs=%s, url_name='%s', app_name='%s', namespace='%s')" % ( 69 69 self.func, self.args, self.kwargs, self.url_name, self.app_name, self.namespace) 70 70 71 class 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 71 109 class Resolver404(Http404): 72 110 pass 73 111 112 class ContinueResolving(Resolver404): 113 pass 114 74 115 class NoReverseMatch(Exception): 75 116 # Don't make this raise an error when used in a template. 76 117 silent_variable_failure = True … … class RegexURLResolver(LocaleRegexProvider): 291 332 return self._app_dict[language_code] 292 333 293 334 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 295 344 match = self.regex.search(path) 296 345 if match: 297 346 new_path = path[match.end():] 298 347 for pattern in self.url_patterns: 348 sub_tried = [] 349 sub_matches = [] 299 350 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] 301 357 except Resolver404, e: 302 358 sub_tried = e.args[0].get('tried') 303 359 if sub_tried is not None: … … class RegexURLResolver(LocaleRegexProvider): 305 361 else: 306 362 tried.append([pattern]) 307 363 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]) 315 382 raise Resolver404({'tried': tried, 'path': new_path}) 316 383 raise Resolver404({'path' : path}) 317 384 … … def resolve(path, urlconf=None): 415 482 urlconf = get_urlconf() 416 483 return get_resolver(urlconf).resolve(path) 417 484 485 def backtracking_resolve(path, urlconf=None): 486 if urlconf is None: 487 urlconf = get_urlconf() 488 return get_resolver(urlconf).backtracking_resolve(path) 489 418 490 def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None): 419 491 if urlconf is None: 420 492 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: 52 52 ``urlpatterns``. This should be a Python list, in the format returned by 53 53 the function :func:`django.conf.urls.defaults.patterns`. 54 54 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. 62 68 63 69 Example 64 70 ======= -
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): 866 866 content_type="application/json; charset=koi8-r") 867 867 self.assertEqual(response.content, json.encode('koi8-r')) 868 868 869 class 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 869 885 class DummyFile(object): 870 886 def __init__(self, filename): 871 887 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 3 3 import views 4 4 5 5 urlpatterns = patterns('', 6 (r'^(?P<data>.*)$', views.backtracking_resolve_dynamic_capture), 6 7 (r'^no_template_view/$', views.no_template_view), 7 8 (r'^staff_only/$', views.staff_only_view), 8 9 (r'^get_view/$', views.get_view), … … urlpatterns = patterns('', 16 17 (r'^redirect_to_non_existent_view/$', RedirectView.as_view(url='/test_client_regress/non_existent_view/')), 17 18 (r'^redirect_to_non_existent_view2/$', RedirectView.as_view(url='/test_client_regress/redirect_to_non_existent_view/')), 18 19 (r'^redirect_to_self/$', RedirectView.as_view(url='/test_client_regress/redirect_to_self/')), 20 (r'^(?P<data>.*)$', views.backtracking_resolve_dynamic_redirect), 19 21 (r'^circular_redirect_1/$', RedirectView.as_view(url='/test_client_regress/circular_redirect_2/')), 20 22 (r'^circular_redirect_2/$', RedirectView.as_view(url='/test_client_regress/circular_redirect_3/')), 21 23 (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 1 1 from django.conf import settings 2 2 from django.contrib.auth.decorators import login_required 3 from django.core.urlresolvers import ContinueResolving, reverse 3 4 from django.http import HttpResponse, HttpResponseRedirect 4 5 from django.core.exceptions import SuspiciousOperation 5 6 from django.shortcuts import render_to_response … … from django.core.serializers.json import DjangoJSONEncoder 9 10 from django.test.client import CONTENT_TYPE_RE 10 11 from django.template import RequestContext 11 12 13 14 def 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 19 def 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 12 24 def no_template_view(request): 13 25 "A simple view that expects a GET request, and returns a rendered template" 14 26 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
- + 1 from django.conf.urls.defaults import * 2 from views import continue_resolving_view, backtracking_view 3 4 urlpatterns = 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. 3 3 """ 4 4 from django.conf import settings 5 5 from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist 6 from django.core.urlresolvers import ( reverse, resolve, NoReverseMatch,7 Resolver404, ResolverMatch, RegexURLResolver, RegexURLPattern)6 from django.core.urlresolvers import (backtracking_resolve, reverse, resolve, 7 NoReverseMatch, Resolver404, ResolverMatch, RegexURLResolver, RegexURLPattern) 8 8 from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect 9 9 from django.shortcuts import redirect 10 10 from django.test import TestCase … … class URLPatternReverse(TestCase): 164 164 # Reversing None should raise an error, not return the last un-named view. 165 165 self.assertRaises(NoReverseMatch, reverse, None) 166 166 167 class 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 167 185 class ResolverTests(unittest.TestCase): 168 186 def test_non_regex(self): 169 187 """ -
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 1 from django.core.urlresolvers import ContinueResolving 1 2 from django.http import HttpResponse 2 3 from django.views.generic import RedirectView 3 4 from django.core.urlresolvers import reverse_lazy … … def kwargs_view(request, arg1=1, arg2=2): 13 14 def absolute_kwargs_view(request, arg1=1, arg2=2): 14 15 return HttpResponse('') 15 16 17 def continue_resolving_view(request, *args, **kwargs): 18 raise ContinueResolving 19 20 def backtracking_view(request, *args, **kwargs): 21 return HttpResponse('backtracked ok') 22 16 23 def defaults_view(request, arg1, arg2): 17 24 pass 18 25