diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py
index e6ef6e2f9e..f144ce4bb1 100644
--- a/django/core/handlers/base.py
+++ b/django/core/handlers/base.py
@@ -68,6 +68,9 @@ class BaseHandler(object):
from django.core import exceptions, urlresolvers
from django.conf import settings
+ # Reset the urlconf for this thread.
+ urlresolvers.set_urlconf(None)
+
# Apply request middleware
for middleware_method in self._request_middleware:
response = middleware_method(request)
@@ -77,61 +80,69 @@ class BaseHandler(object):
# Get urlconf from request object, if available. Otherwise use default.
urlconf = getattr(request, "urlconf", settings.ROOT_URLCONF)
+ # Set the urlconf for this thread to the one specified above.
+ urlresolvers.set_urlconf(urlconf)
+
resolver = urlresolvers.RegexURLResolver(r'^/', urlconf)
try:
- callback, callback_args, callback_kwargs = resolver.resolve(
- request.path_info)
-
- # Apply view middleware
- for middleware_method in self._view_middleware:
- response = middleware_method(request, callback, callback_args, callback_kwargs)
- if response:
- return response
-
try:
- response = callback(request, *callback_args, **callback_kwargs)
- except Exception, e:
- # If the view raised an exception, run it through exception
- # middleware, and if the exception middleware returns a
- # response, use that. Otherwise, reraise the exception.
- for middleware_method in self._exception_middleware:
- response = middleware_method(request, e)
+ callback, callback_args, callback_kwargs = resolver.resolve(
+ request.path_info)
+
+ # Apply view middleware
+ for middleware_method in self._view_middleware:
+ response = middleware_method(request, callback, callback_args, callback_kwargs)
if response:
return response
- raise
- # Complain if the view returned None (a common error).
- if response is None:
try:
- view_name = callback.func_name # If it's a function
- except AttributeError:
- view_name = callback.__class__.__name__ + '.__call__' # If it's a class
- raise ValueError, "The view %s.%s didn't return an HttpResponse object." % (callback.__module__, view_name)
+ response = callback(request, *callback_args, **callback_kwargs)
+ except Exception, e:
+ # If the view raised an exception, run it through exception
+ # middleware, and if the exception middleware returns a
+ # response, use that. Otherwise, reraise the exception.
+ for middleware_method in self._exception_middleware:
+ response = middleware_method(request, e)
+ if response:
+ return response
+ raise
- return response
- except http.Http404, e:
- if settings.DEBUG:
- from django.views import debug
- return debug.technical_404_response(request, e)
- else:
- try:
- callback, param_dict = resolver.resolve404()
- return callback(request, **param_dict)
- except:
+ # Complain if the view returned None (a common error).
+ if response is None:
try:
- return self.handle_uncaught_exception(request, resolver, sys.exc_info())
- finally:
- receivers = signals.got_request_exception.send(sender=self.__class__, request=request)
- except exceptions.PermissionDenied:
- return http.HttpResponseForbidden('
Permission denied
')
- except SystemExit:
- # Allow sys.exit() to actually exit. See tickets #1023 and #4701
- raise
- except: # Handle everything else, including SuspiciousOperation, etc.
- # Get the exception info now, in case another exception is thrown later.
- exc_info = sys.exc_info()
- receivers = signals.got_request_exception.send(sender=self.__class__, request=request)
- return self.handle_uncaught_exception(request, resolver, exc_info)
+ view_name = callback.func_name # If it's a function
+ except AttributeError:
+ view_name = callback.__class__.__name__ + '.__call__' # If it's a class
+ raise ValueError, "The view %s.%s didn't return an HttpResponse object." % (callback.__module__, view_name)
+
+ return response
+ except http.Http404, e:
+ if settings.DEBUG:
+ from django.views import debug
+ return debug.technical_404_response(request, e)
+ else:
+ try:
+ callback, param_dict = resolver.resolve404()
+ return callback(request, **param_dict)
+ except:
+ try:
+ return self.handle_uncaught_exception(request, resolver, sys.exc_info())
+ finally:
+ receivers = signals.got_request_exception.send(sender=self.__class__, request=request)
+ except exceptions.PermissionDenied:
+ return http.HttpResponseForbidden('Permission denied
')
+ except SystemExit:
+ # Allow sys.exit() to actually exit. See tickets #1023 and #4701
+ raise
+ except: # Handle everything else, including SuspiciousOperation, etc.
+ # Get the exception info now, in case another exception is thrown later.
+ exc_info = sys.exc_info()
+ receivers = signals.got_request_exception.send(sender=self.__class__, request=request)
+ return self.handle_uncaught_exception(request, resolver, exc_info)
+ finally:
+ # Reset URLconf for this thread on the way out for complete
+ # isolation of request.urlconf
+ urlresolvers.set_urlconf(None)
def handle_uncaught_exception(self, request, resolver, exc_info):
"""
diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py
index eedb8f126c..a924afeaf8 100644
--- a/django/core/urlresolvers.py
+++ b/django/core/urlresolvers.py
@@ -10,6 +10,7 @@ a string) and returns a tuple in this format:
import re
from django.http import Http404
+from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
from django.utils.datastructures import MultiValueDict
from django.utils.encoding import iri_to_uri, force_unicode, smart_str
@@ -32,6 +33,9 @@ _callable_cache = {} # Maps view and url pattern names to their view functions.
# be empty.
_prefixes = {}
+# Overridden URLconfs for each thread are stored here.
+_urlconfs = {}
+
class Resolver404(Http404):
pass
@@ -300,9 +304,13 @@ class RegexURLResolver(object):
"arguments '%s' not found." % (lookup_view_s, args, kwargs))
def resolve(path, urlconf=None):
+ if urlconf is None:
+ urlconf = get_urlconf()
return get_resolver(urlconf).resolve(path)
def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None):
+ if urlconf is None:
+ urlconf = get_urlconf()
resolver = get_resolver(urlconf)
args = args or []
kwargs = kwargs or {}
@@ -371,3 +379,25 @@ def get_script_prefix():
"""
return _prefixes.get(currentThread(), u'/')
+def set_urlconf(urlconf_name):
+ """
+ Sets the URLconf for the current thread (overriding the default one in
+ settings). Set to None to revert back to the default.
+ """
+ thread = currentThread()
+ if urlconf_name:
+ _urlconfs[thread] = urlconf_name
+ else:
+ # faster than wrapping in a try/except
+ if thread in _urlconfs:
+ del _urlconfs[thread]
+
+def get_urlconf(default=None):
+ """
+ Returns the root URLconf to use for the current thread if it has been
+ changed from the default one.
+ """
+ thread = currentThread()
+ if thread in _urlconfs:
+ return _urlconfs[thread]
+ return default
diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt
index 0b2257cefe..fd45e79876 100644
--- a/docs/topics/http/urls.txt
+++ b/docs/topics/http/urls.txt
@@ -40,7 +40,8 @@ algorithm the system follows to determine which Python code to execute:
1. Django determines the root URLconf module to use. Ordinarily,
this is the value of the ``ROOT_URLCONF`` setting, but if the incoming
- ``HttpRequest`` object has an attribute called ``urlconf``, its value
+ ``HttpRequest`` object has an attribute called ``urlconf`` (set by
+ middleware :ref:`request processing `), its value
will be used in place of the ``ROOT_URLCONF`` setting.
2. Django loads that Python module and looks for the variable
diff --git a/tests/regressiontests/urlpatterns_reverse/middleware.py b/tests/regressiontests/urlpatterns_reverse/middleware.py
new file mode 100644
index 0000000000..7bb9aa924f
--- /dev/null
+++ b/tests/regressiontests/urlpatterns_reverse/middleware.py
@@ -0,0 +1,7 @@
+from django.core.urlresolvers import set_urlconf
+
+import urlconf_inner
+
+class ChangeURLconfMiddleware(object):
+ def process_request(self, request):
+ request.urlconf = urlconf_inner.__name__
diff --git a/tests/regressiontests/urlpatterns_reverse/tests.py b/tests/regressiontests/urlpatterns_reverse/tests.py
index d4f281ba81..34c9db25d3 100644
--- a/tests/regressiontests/urlpatterns_reverse/tests.py
+++ b/tests/regressiontests/urlpatterns_reverse/tests.py
@@ -16,11 +16,16 @@ ImproperlyConfigured: The included urlconf regressiontests.urlpatterns_reverse.n
import unittest
+from django.conf import settings
from django.core.urlresolvers import reverse, resolve, NoReverseMatch, Resolver404
from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
from django.shortcuts import redirect
from django.test import TestCase
+import urlconf_outer
+import urlconf_inner
+import middleware
+
test_data = (
('places', '/places/3/', [3], {}),
('places', '/places/3/', ['3'], {}),
@@ -239,3 +244,35 @@ class NamespaceTests(TestCase):
self.assertEquals('/other1/inner/37/42/', reverse('nodefault:urlobject-view', args=[37,42], current_app='other-ns1'))
self.assertEquals('/other1/inner/42/37/', reverse('nodefault:urlobject-view', kwargs={'arg1':42, 'arg2':37}, current_app='other-ns1'))
+
+class RequestURLconfTests(TestCase):
+ def setUp(self):
+ self.root_urlconf = settings.ROOT_URLCONF
+ self.middleware_classes = settings.MIDDLEWARE_CLASSES
+ settings.ROOT_URLCONF = urlconf_outer.__name__
+
+ def tearDown(self):
+ settings.ROOT_URLCONF = self.root_urlconf
+ settings.MIDDLEWARE_CLASSES = self.middleware_classes
+
+ def test_urlconf(self):
+ response = self.client.get('/test/me/')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.content, 'outer:/test/me/,'
+ 'inner:/inner_urlconf/second_test/')
+ response = self.client.get('/inner_urlconf/second_test/')
+ self.assertEqual(response.status_code, 200)
+ response = self.client.get('/second_test/')
+ self.assertEqual(response.status_code, 404)
+
+ def test_urlconf_overridden(self):
+ settings.MIDDLEWARE_CLASSES += (
+ '%s.ChangeURLconfMiddleware' % middleware.__name__,
+ )
+ response = self.client.get('/test/me/')
+ self.assertEqual(response.status_code, 404)
+ response = self.client.get('/inner_urlconf/second_test/')
+ self.assertEqual(response.status_code, 404)
+ response = self.client.get('/second_test/')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.content, 'outer:,inner:/second_test/')
diff --git a/tests/regressiontests/urlpatterns_reverse/urlconf_inner.py b/tests/regressiontests/urlpatterns_reverse/urlconf_inner.py
new file mode 100644
index 0000000000..d188e06421
--- /dev/null
+++ b/tests/regressiontests/urlpatterns_reverse/urlconf_inner.py
@@ -0,0 +1,12 @@
+from django.conf.urls.defaults import *
+from django.template import Template, Context
+from django.http import HttpResponse
+
+def inner_view(request):
+ content = Template('{% url outer as outer_url %}outer:{{ outer_url }},'
+ '{% url inner as inner_url %}inner:{{ inner_url }}').render(Context())
+ return HttpResponse(content)
+
+urlpatterns = patterns('',
+ url(r'^second_test/$', inner_view, name='inner'),
+)
\ No newline at end of file
diff --git a/tests/regressiontests/urlpatterns_reverse/urlconf_outer.py b/tests/regressiontests/urlpatterns_reverse/urlconf_outer.py
new file mode 100644
index 0000000000..506e03666e
--- /dev/null
+++ b/tests/regressiontests/urlpatterns_reverse/urlconf_outer.py
@@ -0,0 +1,9 @@
+from django.conf.urls.defaults import *
+
+import urlconf_inner
+
+
+urlpatterns = patterns('',
+ url(r'^test/me/$', urlconf_inner.inner_view, name='outer'),
+ url(r'^inner_urlconf/', include(urlconf_inner.__name__))
+)
\ No newline at end of file