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:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user