Skip to content

Commit

Permalink
Fixed #13922 -- Updated resolve() to support namespaces. Thanks to No…
Browse files Browse the repository at this point in the history
…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
freakboy3742 committed Aug 5, 2010
1 parent aa93f8c commit e0fb90b
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 11 deletions.
35 changes: 32 additions & 3 deletions django/core/urlresolvers.py
Expand Up @@ -30,6 +30,35 @@
# 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


Expand Down Expand Up @@ -120,7 +149,7 @@ def resolve(self, path):
# 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:
Expand Down Expand Up @@ -224,9 +253,9 @@ def resolve(self, path):
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})
Expand Down
78 changes: 71 additions & 7 deletions docs/topics/http/urls.txt
Expand Up @@ -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
Expand All @@ -858,6 +921,7 @@ error before redirecting to it::
return HttpResponseRedirect('/') return HttpResponseRedirect('/')
return response return response



permalink() permalink()
----------- -----------


Expand Down
Expand Up @@ -7,7 +7,11 @@
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')),
) )


3 changes: 3 additions & 0 deletions tests/regressiontests/urlpatterns_reverse/namespace_urls.py
Expand Up @@ -23,6 +23,9 @@ def urls(self):
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)),
Expand Down
63 changes: 62 additions & 1 deletion tests/regressiontests/urlpatterns_reverse/tests.py
Expand Up @@ -18,14 +18,43 @@


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


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], {}),
Expand Down Expand Up @@ -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/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'))
Expand Down Expand Up @@ -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)

0 comments on commit e0fb90b

Please sign in to comment.