Fixed #13922 -- Updated resolve() to support namespaces. Thanks to Nowell Strite for the report and patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@13479 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Russell Keith-Magee 2010-08-05 07:09:47 +00:00
parent aa93f8c2f0
commit e0fb90b2f3
5 changed files with 172 additions and 11 deletions

View File

@ -30,6 +30,35 @@ _prefixes = {}
# Overridden URLconfs for each thread are stored here. # Overridden URLconfs for each thread are stored here.
_urlconfs = {} _urlconfs = {}
class ResolverMatch(object):
def __init__(self, func, args, kwargs, url_name=None, app_name=None, namespaces=None):
self.func = func
self.args = args
self.kwargs = kwargs
self.app_name = app_name
if namespaces:
self.namespaces = [x for x in namespaces if x]
else:
self.namespaces = []
if not url_name:
url_name = '.'.join([ func.__module__, func.__name__ ])
self.url_name = url_name
def namespace(self):
return ':'.join(self.namespaces)
namespace = property(namespace)
def view_name(self):
return ':'.join([ x for x in [ self.namespace, self.url_name ] if x ])
view_name = property(view_name)
def __getitem__(self, index):
return (self.func, self.args, self.kwargs)[index]
def __repr__(self):
return "ResolverMatch(func=%s, args=%s, kwargs=%s, url_name='%s', app_name='%s', namespace='%s')" % (
self.func, self.args, self.kwargs, self.url_name, self.app_name, self.namespace)
class Resolver404(Http404): class Resolver404(Http404):
pass pass
@ -120,7 +149,7 @@ class RegexURLPattern(object):
# In both cases, pass any extra_kwargs as **kwargs. # In both cases, pass any extra_kwargs as **kwargs.
kwargs.update(self.default_args) kwargs.update(self.default_args)
return self.callback, args, kwargs return ResolverMatch(self.callback, args, kwargs, self.name)
def _get_callback(self): def _get_callback(self):
if self._callback is not None: if self._callback is not None:
@ -224,9 +253,9 @@ class RegexURLResolver(object):
if sub_match: if sub_match:
sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()]) sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()])
sub_match_dict.update(self.default_kwargs) sub_match_dict.update(self.default_kwargs)
for k, v in sub_match[2].iteritems(): for k, v in sub_match.kwargs.iteritems():
sub_match_dict[smart_str(k)] = v sub_match_dict[smart_str(k)] = v
return sub_match[0], sub_match[1], sub_match_dict 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)
tried.append(pattern.regex.pattern) tried.append(pattern.regex.pattern)
raise Resolver404({'tried': tried, 'path': new_path}) raise Resolver404({'tried': tried, 'path': new_path})
raise Resolver404({'path' : path}) raise Resolver404({'path' : path})

View File

@ -827,17 +827,80 @@ namespaces into URLs on specific application instances, according to the
resolve() resolve()
--------- ---------
The :func:`django.core.urlresolvers.resolve` function can be used for resolving The :func:`django.core.urlresolvers.resolve` function can be used for
URL paths to the corresponding view functions. It has the following signature: resolving URL paths to the corresponding view functions. It has the
following signature:
.. function:: resolve(path, urlconf=None) .. function:: resolve(path, urlconf=None)
``path`` is the URL path you want to resolve. As with ``reverse()`` above, you ``path`` is the URL path you want to resolve. As with
don't need to worry about the ``urlconf`` parameter. The function returns the :func:`~django.core.urlresolvers.reverse`, you don't need to
triple (view function, arguments, keyword arguments). worry about the ``urlconf`` parameter. The function returns a
:class:`django.core.urlresolvers.ResolverMatch` object that allows you
to access various meta-data about the resolved URL.
For example, it can be used for testing if a view would raise a ``Http404`` .. class:: ResolverMatch()
error before redirecting to it::
.. attribute:: ResolverMatch.func
The view function that would be used to serve the URL
.. attribute:: ResolverMatch.args
The arguments that would be passed to the view function, as
parsed from the URL.
.. attribute:: ResolverMatch.kwargs
The keyword arguments that would be passed to the view
function, as parsed from the URL.
.. attribute:: ResolverMatch.url_name
The name of the URL pattern that matches the URL.
.. attribute:: ResolverMatch.app_name
The application namespace for the URL pattern that matches the
URL.
.. attribute:: ResolverMatch.namespace
The instance namespace for the URL pattern that matches the
URL.
.. attribute:: ResolverMatch.namespaces
The list of individual namespace components in the full
instance namespace for the URL pattern that matches the URL.
i.e., if the namespace is ``foo:bar``, then namespaces will be
``[`foo`, `bar`]``.
A :class:`~django.core.urlresolvers.ResolverMatch` object can then be
interrogated to provide information about the URL pattern that matches
a URL::
# Resolve a URL
match = resolve('/some/path/')
# Print the URL pattern that matches the URL
print match.url_name
A :class:`~django.core.urlresolvers.ResolverMatch` object can also be
assigned to a triple::
func, args, kwargs = resolve('/some/path/')
.. versionchanged:: 1.3
Triple-assignment exists for backwards-compatibility. Prior to
Django 1.3, :func:`~django.core.urlresolvers.resolve` returned a
triple containing (view function, arguments, keyword arguments);
the :class:`~django.core.urlresolvers.ResolverMatch` object (as
well as the namespace and pattern information it provides) is not
available in earlier Django releases.
One possible use of :func:`~django.core.urlresolvers.resolve` would be
to testing if a view would raise a ``Http404`` error before
redirecting to it::
from urlparse import urlparse from urlparse import urlparse
from django.core.urlresolvers import resolve from django.core.urlresolvers import resolve
@ -858,6 +921,7 @@ error before redirecting to it::
return HttpResponseRedirect('/') return HttpResponseRedirect('/')
return response return response
permalink() permalink()
----------- -----------

View File

@ -7,7 +7,11 @@ urlpatterns = patterns('regressiontests.urlpatterns_reverse.views',
url(r'^normal/$', 'empty_view', name='inc-normal-view'), url(r'^normal/$', 'empty_view', name='inc-normal-view'),
url(r'^normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='inc-normal-view'), url(r'^normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='inc-normal-view'),
url(r'^mixed_args/(\d+)/(?P<arg2>\d+)/$', 'empty_view', name='inc-mixed-args'),
url(r'^no_kwargs/(\d+)/(\d+)/$', 'empty_view', name='inc-no-kwargs'),
(r'^test3/', include(testobj3.urls)), (r'^test3/', include(testobj3.urls)),
(r'^ns-included3/', include('regressiontests.urlpatterns_reverse.included_urls', namespace='inc-ns3')), (r'^ns-included3/', include('regressiontests.urlpatterns_reverse.included_urls', namespace='inc-ns3')),
(r'^ns-included4/', include('regressiontests.urlpatterns_reverse.namespace_urls', namespace='inc-ns4')),
) )

View File

@ -23,6 +23,9 @@ urlpatterns = patterns('regressiontests.urlpatterns_reverse.views',
url(r'^normal/$', 'empty_view', name='normal-view'), url(r'^normal/$', 'empty_view', name='normal-view'),
url(r'^normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='normal-view'), url(r'^normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='normal-view'),
url(r'^mixed_args/(\d+)/(?P<arg2>\d+)/$', 'empty_view', name='mixed-args'),
url(r'^no_kwargs/(\d+)/(\d+)/$', 'empty_view', name='no-kwargs'),
(r'^test1/', include(testobj1.urls)), (r'^test1/', include(testobj1.urls)),
(r'^test2/', include(testobj2.urls)), (r'^test2/', include(testobj2.urls)),
(r'^default/', include(default_testobj.urls)), (r'^default/', include(default_testobj.urls)),

View File

@ -18,7 +18,7 @@ import unittest
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse, resolve, NoReverseMatch, Resolver404 from django.core.urlresolvers import reverse, resolve, NoReverseMatch, Resolver404, ResolverMatch
from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
from django.shortcuts import redirect from django.shortcuts import redirect
from django.test import TestCase from django.test import TestCase
@ -26,6 +26,35 @@ from django.test import TestCase
import urlconf_outer import urlconf_outer
import urlconf_inner import urlconf_inner
import middleware import middleware
import views
resolve_test_data = (
# These entries are in the format: (path, url_name, app_name, namespace, view_func, args, kwargs)
# Simple case
('/normal/42/37/', 'normal-view', None, '', views.empty_view, tuple(), {'arg1': '42', 'arg2': '37'}),
('/included/normal/42/37/', 'inc-normal-view', None, '', views.empty_view, tuple(), {'arg1': '42', 'arg2': '37'}),
# Unnamed args are dropped if you have *any* kwargs in a pattern
('/mixed_args/42/37/', 'mixed-args', None, '', views.empty_view, tuple(), {'arg2': '37'}),
('/included/mixed_args/42/37/', 'inc-mixed-args', None, '', views.empty_view, tuple(), {'arg2': '37'}),
# If you have no kwargs, you get an args list.
('/no_kwargs/42/37/', 'no-kwargs', None, '', views.empty_view, ('42','37'), {}),
('/included/no_kwargs/42/37/', 'inc-no-kwargs', None, '', views.empty_view, ('42','37'), {}),
# Namespaces
('/test1/inner/42/37/', 'urlobject-view', 'testapp', 'test-ns1', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
('/included/test3/inner/42/37/', 'urlobject-view', 'testapp', 'test-ns3', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
('/ns-included1/normal/42/37/', 'inc-normal-view', None, 'inc-ns1', views.empty_view, tuple(), {'arg1': '42', 'arg2': '37'}),
('/included/test3/inner/42/37/', 'urlobject-view', 'testapp', 'test-ns3', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
('/default/inner/42/37/', 'urlobject-view', 'testapp', 'testapp', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
('/other2/inner/42/37/', 'urlobject-view', 'nodefault', 'other-ns2', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
('/other1/inner/42/37/', 'urlobject-view', 'nodefault', 'other-ns1', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
# Nested namespaces
('/ns-included1/test3/inner/42/37/', 'urlobject-view', 'testapp', 'inc-ns1:test-ns3', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
('/ns-included1/ns-included4/ns-included2/test3/inner/42/37/', 'urlobject-view', 'testapp', 'inc-ns1:inc-ns4:inc-ns2:test-ns3', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
)
test_data = ( test_data = (
('places', '/places/3/', [3], {}), ('places', '/places/3/', [3], {}),
@ -229,6 +258,12 @@ class NamespaceTests(TestCase):
self.assertEquals('/ns-included1/test3/inner/37/42/', reverse('inc-ns1:test-ns3:urlobject-view', args=[37,42])) self.assertEquals('/ns-included1/test3/inner/37/42/', reverse('inc-ns1:test-ns3:urlobject-view', args=[37,42]))
self.assertEquals('/ns-included1/test3/inner/42/37/', reverse('inc-ns1:test-ns3:urlobject-view', kwargs={'arg1':42, 'arg2':37})) self.assertEquals('/ns-included1/test3/inner/42/37/', reverse('inc-ns1:test-ns3:urlobject-view', kwargs={'arg1':42, 'arg2':37}))
def test_nested_namespace_pattern(self):
"Namespaces can be nested"
self.assertEquals('/ns-included1/ns-included4/ns-included1/test3/inner/', reverse('inc-ns1:inc-ns4:inc-ns1:test-ns3:urlobject-view'))
self.assertEquals('/ns-included1/ns-included4/ns-included1/test3/inner/37/42/', reverse('inc-ns1:inc-ns4:inc-ns1:test-ns3:urlobject-view', args=[37,42]))
self.assertEquals('/ns-included1/ns-included4/ns-included1/test3/inner/42/37/', reverse('inc-ns1:inc-ns4:inc-ns1:test-ns3:urlobject-view', kwargs={'arg1':42, 'arg2':37}))
def test_app_lookup_object(self): def test_app_lookup_object(self):
"A default application namespace can be used for lookup" "A default application namespace can be used for lookup"
self.assertEquals('/default/inner/', reverse('testapp:urlobject-view')) self.assertEquals('/default/inner/', reverse('testapp:urlobject-view'))
@ -317,3 +352,29 @@ class NoRootUrlConfTests(TestCase):
def test_no_handler_exception(self): def test_no_handler_exception(self):
self.assertRaises(ImproperlyConfigured, self.client.get, '/test/me/') self.assertRaises(ImproperlyConfigured, self.client.get, '/test/me/')
class ResolverMatchTests(TestCase):
urls = 'regressiontests.urlpatterns_reverse.namespace_urls'
def test_urlpattern_resolve(self):
for path, name, app_name, namespace, func, args, kwargs in resolve_test_data:
# Test legacy support for extracting "function, args, kwargs"
match_func, match_args, match_kwargs = resolve(path)
self.assertEqual(match_func, func)
self.assertEqual(match_args, args)
self.assertEqual(match_kwargs, kwargs)
# Test ResolverMatch capabilities.
match = resolve(path)
self.assertEqual(match.__class__, ResolverMatch)
self.assertEqual(match.url_name, name)
self.assertEqual(match.args, args)
self.assertEqual(match.kwargs, kwargs)
self.assertEqual(match.app_name, app_name)
self.assertEqual(match.namespace, namespace)
self.assertEqual(match.func, func)
# ... and for legacy purposes:
self.assertEquals(match[0], func)
self.assertEquals(match[1], args)
self.assertEquals(match[2], kwargs)