Permalink
Browse files

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
  • Loading branch information...
1 parent 84ef4a9 commit a63a83e5d88cd1696d1c40e89f254f69116c6800 @malcolmt malcolmt committed Aug 31, 2008
@@ -13,12 +13,14 @@
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 @@ def _get_callback(self):
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 @@ def __repr__(self):
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 @@ def resolve500(self):
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)
Oops, something went wrong. Retry.

0 comments on commit a63a83e

Please sign in to comment.