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.
_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):
pass
@ -120,7 +149,7 @@ class RegexURLPattern(object):
# In both cases, pass any extra_kwargs as **kwargs.
kwargs.update(self.default_args)
return self.callback, args, kwargs
return ResolverMatch(self.callback, args, kwargs, self.name)
def _get_callback(self):
if self._callback is not None:
@ -224,9 +253,9 @@ class RegexURLResolver(object):
if sub_match:
sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()])
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
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)
raise Resolver404({'tried': tried, 'path': new_path})
raise Resolver404({'path' : path})

View File

@ -827,17 +827,80 @@ namespaces into URLs on specific application instances, according to the
resolve()
---------
The :func:`django.core.urlresolvers.resolve` function can be used for resolving
URL paths to the corresponding view functions. It has the following signature:
The :func:`django.core.urlresolvers.resolve` function can be used for
resolving URL paths to the corresponding view functions. It has the
following signature:
.. function:: resolve(path, urlconf=None)
``path`` is the URL path you want to resolve. As with ``reverse()`` above, you
don't need to worry about the ``urlconf`` parameter. The function returns the
triple (view function, arguments, keyword arguments).
``path`` is the URL path you want to resolve. As with
:func:`~django.core.urlresolvers.reverse`, you don't need to
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``
error before redirecting to it::
.. class:: ResolverMatch()
.. 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 django.core.urlresolvers import resolve
@ -858,6 +921,7 @@ error before redirecting to it::
return HttpResponseRedirect('/')
return response
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/(?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'^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/(?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'^test2/', include(testobj2.urls)),
(r'^default/', include(default_testobj.urls)),

View File

@ -18,7 +18,7 @@ import unittest
from django.conf import settings
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.shortcuts import redirect
from django.test import TestCase
@ -26,6 +26,35 @@ from django.test import TestCase
import urlconf_outer
import urlconf_inner
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 = (
('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/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):
"A default application namespace can be used for lookup"
self.assertEquals('/default/inner/', reverse('testapp:urlobject-view'))
@ -317,3 +352,29 @@ class NoRootUrlConfTests(TestCase):
def test_no_handler_exception(self):
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)