app/django/core/urlresolvers.py
changeset 323 ff1a9aa48cfd
parent 54 03e267d67478
child 446 0b479d573a4c
--- a/app/django/core/urlresolvers.py	Tue Oct 14 12:36:55 2008 +0000
+++ b/app/django/core/urlresolvers.py	Tue Oct 14 16:00:59 2008 +0000
@@ -7,20 +7,30 @@
     (view_function, function_args, function_kwargs)
 """
 
+import re
+
 from django.http import Http404
 from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
+from django.utils.datastructures import MultiValueDict
 from django.utils.encoding import iri_to_uri, force_unicode, smart_str
 from django.utils.functional import memoize
-import re
+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.
 
+# SCRIPT_NAME prefixes for each thread are stored here. If there's no entry for
+# the current thread (which is the only one we ever access), it is assumed to
+# be empty.
+_prefixes = {}
+
 class Resolver404(Http404):
     pass
 
@@ -45,6 +55,8 @@
             mod_name, func_name = get_mod_func(lookup_view)
             if func_name != '':
                 lookup_view = getattr(__import__(mod_name, {}, {}, ['']), func_name)
+                if not callable(lookup_view):
+                    raise AttributeError("'%s.%s' is not a callable." % (mod_name, func_name))
         except (ImportError, AttributeError):
             if not can_fail:
                 raise
@@ -69,66 +81,6 @@
         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('$', '')
-
-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.
@@ -185,19 +137,6 @@
         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, AttributeError):
-            raise NoReverseMatch
-        if lookup_view != self.callback:
-            raise NoReverseMatch
-        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.
@@ -206,7 +145,7 @@
         self.urlconf_name = urlconf_name
         self.callback = None
         self.default_kwargs = default_kwargs or {}
-        self._reverse_dict = {}
+        self._reverse_dict = MultiValueDict()
 
     def __repr__(self):
         return '<%s %s %s>' % (self.__class__.__name__, self.urlconf_name, self.regex.pattern)
@@ -214,12 +153,21 @@
     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 in pattern.reverse_dict:
+                        for matches, pat in pattern.reverse_dict.getlist(name):
+                            new_matches = []
+                            for piece, p_args in parent:
+                                new_matches.extend([(piece + suffix, p_args + args) for (suffix, args) in matches])
+                            self._reverse_dict.appendlist(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.appendlist(pattern.callback, (bits, p_pattern))
+                    self._reverse_dict.appendlist(pattern.name, (bits, p_pattern))
         return self._reverse_dict
     reverse_dict = property(_get_reverse_dict)
 
@@ -247,12 +195,7 @@
         try:
             return self._urlconf_module
         except AttributeError:
-            try:
-                self._urlconf_module = __import__(self.urlconf_name, {}, {}, [''])
-            except Exception, e:
-                # Either an invalid urlconf_name, such as "foo.bar.", or some
-                # kind of problem during the actual import.
-                raise ImproperlyConfigured, "Error while importing URLconf %r: %s" % (self.urlconf_name, e)
+            self._urlconf_module = __import__(self.urlconf_name, {}, {}, [''])
             return self._urlconf_module
     urlconf_module = property(_get_urlconf_module)
 
@@ -275,24 +218,60 @@
         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):
-            raise NoReverseMatch
-        if lookup_view in self.reverse_dict:
-            return u''.join([reverse_helper(part.regex, *args, **kwargs) for part in self.reverse_dict[lookup_view]])
-        raise NoReverseMatch
-
-    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
+        except (ImportError, AttributeError), e:
+            raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e))
+        possibilities = self.reverse_dict.getlist(lookup_view)
+        for possibility, pattern in possibilities:
+            for result, params in possibility:
+                if args:
+                    if len(args) != len(params):
+                        continue
+                    unicode_args = [force_unicode(val) for val in args]
+                    candidate =  result % dict(zip(params, unicode_args))
+                else:
+                    if set(kwargs.keys()) != set(params):
+                        continue
+                    unicode_kwargs = dict([(k, force_unicode(v)) for (k, v) in kwargs.items()])
+                    candidate = result % unicode_kwargs
+                if re.search(u'^%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 resolve(path, urlconf=None):
     return get_resolver(urlconf).resolve(path)
 
-def reverse(viewname, urlconf=None, args=None, kwargs=None):
+def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None):
     args = args or []
     kwargs = kwargs or {}
-    return iri_to_uri(u'/' + get_resolver(urlconf).reverse(viewname, *args, **kwargs))
+    if prefix is None:
+        prefix = get_script_prefix()
+    return iri_to_uri(u'%s%s' % (prefix, get_resolver(urlconf).reverse(viewname,
+            *args, **kwargs)))
+
+def clear_url_caches():
+    global _resolver_cache
+    global _callable_cache
+    _resolver_cache.clear()
+    _callable_cache.clear()
 
+def set_script_prefix(prefix):
+    """
+    Sets the script prefix for the current thread.
+    """
+    if not prefix.endswith('/'):
+        prefix += '/'
+    _prefixes[currentThread()] = prefix
+
+def get_script_prefix():
+    """
+    Returns the currently active script prefix. Useful for client code that
+    wishes to construct their own URLs manually (although accessing the request
+    instance is normally going to be a lot cleaner).
+    """
+    return _prefixes.get(currentThread(), u'/')
+