1
0
mirror of https://github.com/django/django.git synced 2025-10-24 22:26:08 +00:00

A rewrite of the reverse URL parsing: the reverse() call and the "url" template tag.

This is fully backwards compatible, but it fixes a bunch of little bugs. Thanks
to SmileyChris and Ilya Semenov for some early patches in this area that were
incorporated into this change.

Fixed #2977, #4915, #6934, #7206.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@8760 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Malcolm Tredinnick
2008-08-31 11:11:20 +00:00
parent 84ef4a9b1d
commit a63a83e5d8
8 changed files with 482 additions and 115 deletions

View File

@@ -13,12 +13,14 @@ from django.http import Http404
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
from django.utils.encoding import iri_to_uri, force_unicode, smart_str
from django.utils.functional import memoize
from django.utils.regex_helper import normalize
from django.utils.thread_support import currentThread
try:
reversed
except NameError:
from django.utils.itercompat import reversed # Python 2.3 fallback
from sets import Set as set
_resolver_cache = {} # Maps urlconf modules to RegexURLResolver instances.
_callable_cache = {} # Maps view and url pattern names to their view functions.
@@ -78,66 +80,6 @@ def get_mod_func(callback):
return callback, ''
return callback[:dot], callback[dot+1:]
def reverse_helper(regex, *args, **kwargs):
"""
Does a "reverse" lookup -- returns the URL for the given args/kwargs.
The args/kwargs are applied to the given compiled regular expression.
For example:
>>> reverse_helper(re.compile('^places/(\d+)/$'), 3)
'places/3/'
>>> reverse_helper(re.compile('^places/(?P<id>\d+)/$'), id=3)
'places/3/'
>>> reverse_helper(re.compile('^people/(?P<state>\w\w)/(\w+)/$'), 'adrian', state='il')
'people/il/adrian/'
Raises NoReverseMatch if the args/kwargs aren't valid for the regex.
"""
# TODO: Handle nested parenthesis in the following regex.
result = re.sub(r'\(([^)]+)\)', MatchChecker(args, kwargs), regex.pattern)
return result.replace('^', '').replace('$', '').replace('\\', '')
class MatchChecker(object):
"Class used in reverse RegexURLPattern lookup."
def __init__(self, args, kwargs):
self.args, self.kwargs = args, kwargs
self.current_arg = 0
def __call__(self, match_obj):
# match_obj.group(1) is the contents of the parenthesis.
# First we need to figure out whether it's a named or unnamed group.
#
grouped = match_obj.group(1)
m = re.search(r'^\?P<(\w+)>(.*?)$', grouped, re.UNICODE)
if m: # If this was a named group...
# m.group(1) is the name of the group
# m.group(2) is the regex.
try:
value = self.kwargs[m.group(1)]
except KeyError:
# It was a named group, but the arg was passed in as a
# positional arg or not at all.
try:
value = self.args[self.current_arg]
self.current_arg += 1
except IndexError:
# The arg wasn't passed in.
raise NoReverseMatch('Not enough positional arguments passed in')
test_regex = m.group(2)
else: # Otherwise, this was a positional (unnamed) group.
try:
value = self.args[self.current_arg]
self.current_arg += 1
except IndexError:
# The arg wasn't passed in.
raise NoReverseMatch('Not enough positional arguments passed in')
test_regex = grouped
# Note we're using re.match here on purpose because the start of
# to string needs to match.
if not re.match(test_regex + '$', force_unicode(value), re.UNICODE):
raise NoReverseMatch("Value %r didn't match regular expression %r" % (value, test_regex))
return force_unicode(value)
class RegexURLPattern(object):
def __init__(self, regex, callback, default_args=None, name=None):
# regex is a string representing a regular expression.
@@ -194,21 +136,6 @@ class RegexURLPattern(object):
return self._callback
callback = property(_get_callback)
def reverse(self, viewname, *args, **kwargs):
mod_name, func_name = get_mod_func(viewname)
try:
lookup_view = getattr(__import__(mod_name, {}, {}, ['']), func_name)
except ImportError, e:
raise NoReverseMatch("Could not import '%s': %s" % (mod_name, e))
except AttributeError, e:
raise NoReverseMatch("'%s' has no attribute '%s'" % (mod_name, func_name))
if lookup_view != self.callback:
raise NoReverseMatch("Reversed view '%s' doesn't match the expected callback ('%s')." % (viewname, self.callback))
return self.reverse_helper(*args, **kwargs)
def reverse_helper(self, *args, **kwargs):
return reverse_helper(self.regex, *args, **kwargs)
class RegexURLResolver(object):
def __init__(self, regex, urlconf_name, default_kwargs=None):
# regex is a string representing a regular expression.
@@ -225,12 +152,21 @@ class RegexURLResolver(object):
def _get_reverse_dict(self):
if not self._reverse_dict and hasattr(self.urlconf_module, 'urlpatterns'):
for pattern in reversed(self.urlconf_module.urlpatterns):
p_pattern = pattern.regex.pattern
if p_pattern.startswith('^'):
p_pattern = p_pattern[1:]
if isinstance(pattern, RegexURLResolver):
for key, value in pattern.reverse_dict.iteritems():
self._reverse_dict[key] = (pattern,) + value
parent = normalize(pattern.regex.pattern)
for name, (matches, pat) in pattern.reverse_dict.iteritems():
new_matches = []
for piece, p_args in parent:
new_matches.extend([(piece + suffix, p_args + args)
for (suffix, args) in matches])
self._reverse_dict[name] = new_matches, p_pattern + pat
else:
self._reverse_dict[pattern.callback] = (pattern,)
self._reverse_dict[pattern.name] = (pattern,)
bits = normalize(p_pattern)
self._reverse_dict[pattern.callback] = bits, p_pattern
self._reverse_dict[pattern.name] = bits, p_pattern
return self._reverse_dict
reverse_dict = property(_get_reverse_dict)
@@ -281,20 +217,27 @@ class RegexURLResolver(object):
return self._resolve_special('500')
def reverse(self, lookup_view, *args, **kwargs):
if args and kwargs:
raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
try:
lookup_view = get_callable(lookup_view, True)
except (ImportError, AttributeError), e:
raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e))
if lookup_view in self.reverse_dict:
return u''.join([reverse_helper(part.regex, *args, **kwargs) for part in self.reverse_dict[lookup_view]])
possibilities, pattern = self.reverse_dict.get(lookup_view, [(), ()])
for result, params in possibilities:
if args:
if len(args) != len(params):
continue
candidate = result % dict(zip(params, args))
else:
if set(kwargs.keys()) != set(params):
continue
candidate = result % kwargs
if re.search('^%s' % pattern, candidate, re.UNICODE):
return candidate
raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword "
"arguments '%s' not found." % (lookup_view, args, kwargs))
def reverse_helper(self, lookup_view, *args, **kwargs):
sub_match = self.reverse(lookup_view, *args, **kwargs)
result = reverse_helper(self.regex, *args, **kwargs)
return result + sub_match
def resolve(path, urlconf=None):
return get_resolver(urlconf).resolve(path)