diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index cad57a5d9ff17..ce064c9926569 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -30,6 +30,35 @@ # 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 @@ def resolve(self, path): # 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 @@ def resolve(self, path): 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}) diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 5a2980f9d2566..d5746c52f298f 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -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() ----------- diff --git a/tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py b/tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py index 073190657c5ed..5dc302c862090 100644 --- a/tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py +++ b/tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py @@ -7,7 +7,11 @@ url(r'^normal/$', 'empty_view', name='inc-normal-view'), url(r'^normal/(?P\d+)/(?P\d+)/$', 'empty_view', name='inc-normal-view'), + url(r'^mixed_args/(\d+)/(?P\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')), ) diff --git a/tests/regressiontests/urlpatterns_reverse/namespace_urls.py b/tests/regressiontests/urlpatterns_reverse/namespace_urls.py index 27cc7f7a225e5..e7991585f065c 100644 --- a/tests/regressiontests/urlpatterns_reverse/namespace_urls.py +++ b/tests/regressiontests/urlpatterns_reverse/namespace_urls.py @@ -23,6 +23,9 @@ def urls(self): url(r'^normal/$', 'empty_view', name='normal-view'), url(r'^normal/(?P\d+)/(?P\d+)/$', 'empty_view', name='normal-view'), + url(r'^mixed_args/(\d+)/(?P\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)), diff --git a/tests/regressiontests/urlpatterns_reverse/tests.py b/tests/regressiontests/urlpatterns_reverse/tests.py index 3fcc935da2f2f..7bf96d17c6e45 100644 --- a/tests/regressiontests/urlpatterns_reverse/tests.py +++ b/tests/regressiontests/urlpatterns_reverse/tests.py @@ -18,7 +18,7 @@ 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 @@ 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 @@ def test_multiple_namespace_pattern(self): 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)