Skip to content

Commit e0fb90b

Browse files
committed
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
1 parent aa93f8c commit e0fb90b

File tree

5 files changed

+172
-11
lines changed

5 files changed

+172
-11
lines changed

django/core/urlresolvers.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,35 @@
3030
# Overridden URLconfs for each thread are stored here.
3131
_urlconfs = {}
3232

33+
class ResolverMatch(object):
34+
def __init__(self, func, args, kwargs, url_name=None, app_name=None, namespaces=None):
35+
self.func = func
36+
self.args = args
37+
self.kwargs = kwargs
38+
self.app_name = app_name
39+
if namespaces:
40+
self.namespaces = [x for x in namespaces if x]
41+
else:
42+
self.namespaces = []
43+
if not url_name:
44+
url_name = '.'.join([ func.__module__, func.__name__ ])
45+
self.url_name = url_name
46+
47+
def namespace(self):
48+
return ':'.join(self.namespaces)
49+
namespace = property(namespace)
50+
51+
def view_name(self):
52+
return ':'.join([ x for x in [ self.namespace, self.url_name ] if x ])
53+
view_name = property(view_name)
54+
55+
def __getitem__(self, index):
56+
return (self.func, self.args, self.kwargs)[index]
57+
58+
def __repr__(self):
59+
return "ResolverMatch(func=%s, args=%s, kwargs=%s, url_name='%s', app_name='%s', namespace='%s')" % (
60+
self.func, self.args, self.kwargs, self.url_name, self.app_name, self.namespace)
61+
3362
class Resolver404(Http404):
3463
pass
3564

@@ -120,7 +149,7 @@ def resolve(self, path):
120149
# In both cases, pass any extra_kwargs as **kwargs.
121150
kwargs.update(self.default_args)
122151

123-
return self.callback, args, kwargs
152+
return ResolverMatch(self.callback, args, kwargs, self.name)
124153

125154
def _get_callback(self):
126155
if self._callback is not None:
@@ -224,9 +253,9 @@ def resolve(self, path):
224253
if sub_match:
225254
sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()])
226255
sub_match_dict.update(self.default_kwargs)
227-
for k, v in sub_match[2].iteritems():
256+
for k, v in sub_match.kwargs.iteritems():
228257
sub_match_dict[smart_str(k)] = v
229-
return sub_match[0], sub_match[1], sub_match_dict
258+
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)
230259
tried.append(pattern.regex.pattern)
231260
raise Resolver404({'tried': tried, 'path': new_path})
232261
raise Resolver404({'path' : path})

docs/topics/http/urls.txt

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -827,17 +827,80 @@ namespaces into URLs on specific application instances, according to the
827827
resolve()
828828
---------
829829

830-
The :func:`django.core.urlresolvers.resolve` function can be used for resolving
831-
URL paths to the corresponding view functions. It has the following signature:
830+
The :func:`django.core.urlresolvers.resolve` function can be used for
831+
resolving URL paths to the corresponding view functions. It has the
832+
following signature:
832833

833834
.. function:: resolve(path, urlconf=None)
834835

835-
``path`` is the URL path you want to resolve. As with ``reverse()`` above, you
836-
don't need to worry about the ``urlconf`` parameter. The function returns the
837-
triple (view function, arguments, keyword arguments).
836+
``path`` is the URL path you want to resolve. As with
837+
:func:`~django.core.urlresolvers.reverse`, you don't need to
838+
worry about the ``urlconf`` parameter. The function returns a
839+
:class:`django.core.urlresolvers.ResolverMatch` object that allows you
840+
to access various meta-data about the resolved URL.
838841

839-
For example, it can be used for testing if a view would raise a ``Http404``
840-
error before redirecting to it::
842+
.. class:: ResolverMatch()
843+
844+
.. attribute:: ResolverMatch.func
845+
846+
The view function that would be used to serve the URL
847+
848+
.. attribute:: ResolverMatch.args
849+
850+
The arguments that would be passed to the view function, as
851+
parsed from the URL.
852+
853+
.. attribute:: ResolverMatch.kwargs
854+
855+
The keyword arguments that would be passed to the view
856+
function, as parsed from the URL.
857+
858+
.. attribute:: ResolverMatch.url_name
859+
860+
The name of the URL pattern that matches the URL.
861+
862+
.. attribute:: ResolverMatch.app_name
863+
864+
The application namespace for the URL pattern that matches the
865+
URL.
866+
867+
.. attribute:: ResolverMatch.namespace
868+
869+
The instance namespace for the URL pattern that matches the
870+
URL.
871+
872+
.. attribute:: ResolverMatch.namespaces
873+
874+
The list of individual namespace components in the full
875+
instance namespace for the URL pattern that matches the URL.
876+
i.e., if the namespace is ``foo:bar``, then namespaces will be
877+
``[`foo`, `bar`]``.
878+
879+
A :class:`~django.core.urlresolvers.ResolverMatch` object can then be
880+
interrogated to provide information about the URL pattern that matches
881+
a URL::
882+
883+
# Resolve a URL
884+
match = resolve('/some/path/')
885+
# Print the URL pattern that matches the URL
886+
print match.url_name
887+
888+
A :class:`~django.core.urlresolvers.ResolverMatch` object can also be
889+
assigned to a triple::
890+
891+
func, args, kwargs = resolve('/some/path/')
892+
893+
.. versionchanged:: 1.3
894+
Triple-assignment exists for backwards-compatibility. Prior to
895+
Django 1.3, :func:`~django.core.urlresolvers.resolve` returned a
896+
triple containing (view function, arguments, keyword arguments);
897+
the :class:`~django.core.urlresolvers.ResolverMatch` object (as
898+
well as the namespace and pattern information it provides) is not
899+
available in earlier Django releases.
900+
901+
One possible use of :func:`~django.core.urlresolvers.resolve` would be
902+
to testing if a view would raise a ``Http404`` error before
903+
redirecting to it::
841904

842905
from urlparse import urlparse
843906
from django.core.urlresolvers import resolve
@@ -858,6 +921,7 @@ error before redirecting to it::
858921
return HttpResponseRedirect('/')
859922
return response
860923

924+
861925
permalink()
862926
-----------
863927

tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
url(r'^normal/$', 'empty_view', name='inc-normal-view'),
88
url(r'^normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='inc-normal-view'),
99

10+
url(r'^mixed_args/(\d+)/(?P<arg2>\d+)/$', 'empty_view', name='inc-mixed-args'),
11+
url(r'^no_kwargs/(\d+)/(\d+)/$', 'empty_view', name='inc-no-kwargs'),
12+
1013
(r'^test3/', include(testobj3.urls)),
1114
(r'^ns-included3/', include('regressiontests.urlpatterns_reverse.included_urls', namespace='inc-ns3')),
15+
(r'^ns-included4/', include('regressiontests.urlpatterns_reverse.namespace_urls', namespace='inc-ns4')),
1216
)
1317

tests/regressiontests/urlpatterns_reverse/namespace_urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ def urls(self):
2323
url(r'^normal/$', 'empty_view', name='normal-view'),
2424
url(r'^normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='normal-view'),
2525

26+
url(r'^mixed_args/(\d+)/(?P<arg2>\d+)/$', 'empty_view', name='mixed-args'),
27+
url(r'^no_kwargs/(\d+)/(\d+)/$', 'empty_view', name='no-kwargs'),
28+
2629
(r'^test1/', include(testobj1.urls)),
2730
(r'^test2/', include(testobj2.urls)),
2831
(r'^default/', include(default_testobj.urls)),

tests/regressiontests/urlpatterns_reverse/tests.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,43 @@
1818

1919
from django.conf import settings
2020
from django.core.exceptions import ImproperlyConfigured
21-
from django.core.urlresolvers import reverse, resolve, NoReverseMatch, Resolver404
21+
from django.core.urlresolvers import reverse, resolve, NoReverseMatch, Resolver404, ResolverMatch
2222
from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
2323
from django.shortcuts import redirect
2424
from django.test import TestCase
2525

2626
import urlconf_outer
2727
import urlconf_inner
2828
import middleware
29+
import views
30+
31+
resolve_test_data = (
32+
# These entries are in the format: (path, url_name, app_name, namespace, view_func, args, kwargs)
33+
# Simple case
34+
('/normal/42/37/', 'normal-view', None, '', views.empty_view, tuple(), {'arg1': '42', 'arg2': '37'}),
35+
('/included/normal/42/37/', 'inc-normal-view', None, '', views.empty_view, tuple(), {'arg1': '42', 'arg2': '37'}),
36+
37+
# Unnamed args are dropped if you have *any* kwargs in a pattern
38+
('/mixed_args/42/37/', 'mixed-args', None, '', views.empty_view, tuple(), {'arg2': '37'}),
39+
('/included/mixed_args/42/37/', 'inc-mixed-args', None, '', views.empty_view, tuple(), {'arg2': '37'}),
40+
41+
# If you have no kwargs, you get an args list.
42+
('/no_kwargs/42/37/', 'no-kwargs', None, '', views.empty_view, ('42','37'), {}),
43+
('/included/no_kwargs/42/37/', 'inc-no-kwargs', None, '', views.empty_view, ('42','37'), {}),
44+
45+
# Namespaces
46+
('/test1/inner/42/37/', 'urlobject-view', 'testapp', 'test-ns1', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
47+
('/included/test3/inner/42/37/', 'urlobject-view', 'testapp', 'test-ns3', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
48+
('/ns-included1/normal/42/37/', 'inc-normal-view', None, 'inc-ns1', views.empty_view, tuple(), {'arg1': '42', 'arg2': '37'}),
49+
('/included/test3/inner/42/37/', 'urlobject-view', 'testapp', 'test-ns3', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
50+
('/default/inner/42/37/', 'urlobject-view', 'testapp', 'testapp', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
51+
('/other2/inner/42/37/', 'urlobject-view', 'nodefault', 'other-ns2', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
52+
('/other1/inner/42/37/', 'urlobject-view', 'nodefault', 'other-ns1', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
53+
54+
# Nested namespaces
55+
('/ns-included1/test3/inner/42/37/', 'urlobject-view', 'testapp', 'inc-ns1:test-ns3', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
56+
('/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'}),
57+
)
2958

3059
test_data = (
3160
('places', '/places/3/', [3], {}),
@@ -229,6 +258,12 @@ def test_multiple_namespace_pattern(self):
229258
self.assertEquals('/ns-included1/test3/inner/37/42/', reverse('inc-ns1:test-ns3:urlobject-view', args=[37,42]))
230259
self.assertEquals('/ns-included1/test3/inner/42/37/', reverse('inc-ns1:test-ns3:urlobject-view', kwargs={'arg1':42, 'arg2':37}))
231260

261+
def test_nested_namespace_pattern(self):
262+
"Namespaces can be nested"
263+
self.assertEquals('/ns-included1/ns-included4/ns-included1/test3/inner/', reverse('inc-ns1:inc-ns4:inc-ns1:test-ns3:urlobject-view'))
264+
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]))
265+
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}))
266+
232267
def test_app_lookup_object(self):
233268
"A default application namespace can be used for lookup"
234269
self.assertEquals('/default/inner/', reverse('testapp:urlobject-view'))
@@ -317,3 +352,29 @@ class NoRootUrlConfTests(TestCase):
317352

318353
def test_no_handler_exception(self):
319354
self.assertRaises(ImproperlyConfigured, self.client.get, '/test/me/')
355+
356+
class ResolverMatchTests(TestCase):
357+
urls = 'regressiontests.urlpatterns_reverse.namespace_urls'
358+
359+
def test_urlpattern_resolve(self):
360+
for path, name, app_name, namespace, func, args, kwargs in resolve_test_data:
361+
# Test legacy support for extracting "function, args, kwargs"
362+
match_func, match_args, match_kwargs = resolve(path)
363+
self.assertEqual(match_func, func)
364+
self.assertEqual(match_args, args)
365+
self.assertEqual(match_kwargs, kwargs)
366+
367+
# Test ResolverMatch capabilities.
368+
match = resolve(path)
369+
self.assertEqual(match.__class__, ResolverMatch)
370+
self.assertEqual(match.url_name, name)
371+
self.assertEqual(match.args, args)
372+
self.assertEqual(match.kwargs, kwargs)
373+
self.assertEqual(match.app_name, app_name)
374+
self.assertEqual(match.namespace, namespace)
375+
self.assertEqual(match.func, func)
376+
377+
# ... and for legacy purposes:
378+
self.assertEquals(match[0], func)
379+
self.assertEquals(match[1], args)
380+
self.assertEquals(match[2], kwargs)

0 commit comments

Comments
 (0)