Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #13922 -- Updated resolve() to support namespaces. Thanks to No…

…well Strite for the report and patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@13479 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit e0fb90b2f3b16300426851790b0ef3e50f3d67b3 1 parent aa93f8c
Russell Keith-Magee authored August 05, 2010
35  django/core/urlresolvers.py
@@ -30,6 +30,35 @@
30 30
 # Overridden URLconfs for each thread are stored here.
31 31
 _urlconfs = {}
32 32
 
  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
+
33 62
 class Resolver404(Http404):
34 63
     pass
35 64
 
@@ -120,7 +149,7 @@ def resolve(self, path):
120 149
             # In both cases, pass any extra_kwargs as **kwargs.
121 150
             kwargs.update(self.default_args)
122 151
 
123  
-            return self.callback, args, kwargs
  152
+            return ResolverMatch(self.callback, args, kwargs, self.name)
124 153
 
125 154
     def _get_callback(self):
126 155
         if self._callback is not None:
@@ -224,9 +253,9 @@ def resolve(self, path):
224 253
                     if sub_match:
225 254
                         sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()])
226 255
                         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():
228 257
                             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)
230 259
                     tried.append(pattern.regex.pattern)
231 260
             raise Resolver404({'tried': tried, 'path': new_path})
232 261
         raise Resolver404({'path' : path})
78  docs/topics/http/urls.txt
@@ -827,17 +827,80 @@ namespaces into URLs on specific application instances, according to the
827 827
 resolve()
828 828
 ---------
829 829
 
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:
832 833
 
833 834
 .. function:: resolve(path, urlconf=None)
834 835
 
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.
838 841
 
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::
841 904
 
842 905
     from urlparse import urlparse
843 906
     from django.core.urlresolvers import resolve
@@ -858,6 +921,7 @@ error before redirecting to it::
858 921
             return HttpResponseRedirect('/')
859 922
         return response
860 923
 
  924
+
861 925
 permalink()
862 926
 -----------
863 927
 
4  tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py
@@ -7,7 +7,11 @@
7 7
     url(r'^normal/$', 'empty_view', name='inc-normal-view'),
8 8
     url(r'^normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='inc-normal-view'),
9 9
 
  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
+
10 13
     (r'^test3/', include(testobj3.urls)),
11 14
     (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')),
12 16
 )
13 17
 
3  tests/regressiontests/urlpatterns_reverse/namespace_urls.py
@@ -23,6 +23,9 @@ def urls(self):
23 23
     url(r'^normal/$', 'empty_view', name='normal-view'),
24 24
     url(r'^normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='normal-view'),
25 25
 
  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
+
26 29
     (r'^test1/', include(testobj1.urls)),
27 30
     (r'^test2/', include(testobj2.urls)),
28 31
     (r'^default/', include(default_testobj.urls)),
63  tests/regressiontests/urlpatterns_reverse/tests.py
@@ -18,7 +18,7 @@
18 18
 
19 19
 from django.conf import settings
20 20
 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
22 22
 from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
23 23
 from django.shortcuts import redirect
24 24
 from django.test import TestCase
@@ -26,6 +26,35 @@
26 26
 import urlconf_outer
27 27
 import urlconf_inner
28 28
 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
+)
29 58
 
30 59
 test_data = (
31 60
     ('places', '/places/3/', [3], {}),
@@ -229,6 +258,12 @@ def test_multiple_namespace_pattern(self):
229 258
         self.assertEquals('/ns-included1/test3/inner/37/42/', reverse('inc-ns1:test-ns3:urlobject-view', args=[37,42]))
230 259
         self.assertEquals('/ns-included1/test3/inner/42/37/', reverse('inc-ns1:test-ns3:urlobject-view', kwargs={'arg1':42, 'arg2':37}))
231 260
 
  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
+
232 267
     def test_app_lookup_object(self):
233 268
         "A default application namespace can be used for lookup"
234 269
         self.assertEquals('/default/inner/', reverse('testapp:urlobject-view'))
@@ -317,3 +352,29 @@ class NoRootUrlConfTests(TestCase):
317 352
 
318 353
     def test_no_handler_exception(self):
319 354
         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 notes on commit e0fb90b

Please sign in to comment.
Something went wrong with that request. Please try again.