web/lib/django/core/urlresolvers.py
changeset 0 0d40e90630ef
child 29 cc9b7e14412b
equal deleted inserted replaced
-1:000000000000 0:0d40e90630ef
       
     1 """
       
     2 This module converts requested URLs to callback view functions.
       
     3 
       
     4 RegexURLResolver is the main class here. Its resolve() method takes a URL (as
       
     5 a string) and returns a tuple in this format:
       
     6 
       
     7     (view_function, function_args, function_kwargs)
       
     8 """
       
     9 
       
    10 import re
       
    11 
       
    12 from django.http import Http404
       
    13 from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
       
    14 from django.utils.datastructures import MultiValueDict
       
    15 from django.utils.encoding import iri_to_uri, force_unicode, smart_str
       
    16 from django.utils.functional import memoize
       
    17 from django.utils.importlib import import_module
       
    18 from django.utils.regex_helper import normalize
       
    19 from django.utils.thread_support import currentThread
       
    20 
       
    21 try:
       
    22     reversed
       
    23 except NameError:
       
    24     from django.utils.itercompat import reversed     # Python 2.3 fallback
       
    25     from sets import Set as set
       
    26 
       
    27 _resolver_cache = {} # Maps URLconf modules to RegexURLResolver instances.
       
    28 _callable_cache = {} # Maps view and url pattern names to their view functions.
       
    29 
       
    30 # SCRIPT_NAME prefixes for each thread are stored here. If there's no entry for
       
    31 # the current thread (which is the only one we ever access), it is assumed to
       
    32 # be empty.
       
    33 _prefixes = {}
       
    34 
       
    35 class Resolver404(Http404):
       
    36     pass
       
    37 
       
    38 class NoReverseMatch(Exception):
       
    39     # Don't make this raise an error when used in a template.
       
    40     silent_variable_failure = True
       
    41 
       
    42 def get_callable(lookup_view, can_fail=False):
       
    43     """
       
    44     Convert a string version of a function name to the callable object.
       
    45 
       
    46     If the lookup_view is not an import path, it is assumed to be a URL pattern
       
    47     label and the original string is returned.
       
    48 
       
    49     If can_fail is True, lookup_view might be a URL pattern label, so errors
       
    50     during the import fail and the string is returned.
       
    51     """
       
    52     if not callable(lookup_view):
       
    53         try:
       
    54             # Bail early for non-ASCII strings (they can't be functions).
       
    55             lookup_view = lookup_view.encode('ascii')
       
    56             mod_name, func_name = get_mod_func(lookup_view)
       
    57             if func_name != '':
       
    58                 lookup_view = getattr(import_module(mod_name), func_name)
       
    59                 if not callable(lookup_view):
       
    60                     raise AttributeError("'%s.%s' is not a callable." % (mod_name, func_name))
       
    61         except (ImportError, AttributeError):
       
    62             if not can_fail:
       
    63                 raise
       
    64         except UnicodeEncodeError:
       
    65             pass
       
    66     return lookup_view
       
    67 get_callable = memoize(get_callable, _callable_cache, 1)
       
    68 
       
    69 def get_resolver(urlconf):
       
    70     if urlconf is None:
       
    71         from django.conf import settings
       
    72         urlconf = settings.ROOT_URLCONF
       
    73     return RegexURLResolver(r'^/', urlconf)
       
    74 get_resolver = memoize(get_resolver, _resolver_cache, 1)
       
    75 
       
    76 def get_mod_func(callback):
       
    77     # Converts 'django.views.news.stories.story_detail' to
       
    78     # ['django.views.news.stories', 'story_detail']
       
    79     try:
       
    80         dot = callback.rindex('.')
       
    81     except ValueError:
       
    82         return callback, ''
       
    83     return callback[:dot], callback[dot+1:]
       
    84 
       
    85 class RegexURLPattern(object):
       
    86     def __init__(self, regex, callback, default_args=None, name=None):
       
    87         # regex is a string representing a regular expression.
       
    88         # callback is either a string like 'foo.views.news.stories.story_detail'
       
    89         # which represents the path to a module and a view function name, or a
       
    90         # callable object (view).
       
    91         self.regex = re.compile(regex, re.UNICODE)
       
    92         if callable(callback):
       
    93             self._callback = callback
       
    94         else:
       
    95             self._callback = None
       
    96             self._callback_str = callback
       
    97         self.default_args = default_args or {}
       
    98         self.name = name
       
    99 
       
   100     def __repr__(self):
       
   101         return '<%s %s %s>' % (self.__class__.__name__, self.name, self.regex.pattern)
       
   102 
       
   103     def add_prefix(self, prefix):
       
   104         """
       
   105         Adds the prefix string to a string-based callback.
       
   106         """
       
   107         if not prefix or not hasattr(self, '_callback_str'):
       
   108             return
       
   109         self._callback_str = prefix + '.' + self._callback_str
       
   110 
       
   111     def resolve(self, path):
       
   112         match = self.regex.search(path)
       
   113         if match:
       
   114             # If there are any named groups, use those as kwargs, ignoring
       
   115             # non-named groups. Otherwise, pass all non-named arguments as
       
   116             # positional arguments.
       
   117             kwargs = match.groupdict()
       
   118             if kwargs:
       
   119                 args = ()
       
   120             else:
       
   121                 args = match.groups()
       
   122             # In both cases, pass any extra_kwargs as **kwargs.
       
   123             kwargs.update(self.default_args)
       
   124 
       
   125             return self.callback, args, kwargs
       
   126 
       
   127     def _get_callback(self):
       
   128         if self._callback is not None:
       
   129             return self._callback
       
   130         try:
       
   131             self._callback = get_callable(self._callback_str)
       
   132         except ImportError, e:
       
   133             mod_name, _ = get_mod_func(self._callback_str)
       
   134             raise ViewDoesNotExist, "Could not import %s. Error was: %s" % (mod_name, str(e))
       
   135         except AttributeError, e:
       
   136             mod_name, func_name = get_mod_func(self._callback_str)
       
   137             raise ViewDoesNotExist, "Tried %s in module %s. Error was: %s" % (func_name, mod_name, str(e))
       
   138         return self._callback
       
   139     callback = property(_get_callback)
       
   140 
       
   141 class RegexURLResolver(object):
       
   142     def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
       
   143         # regex is a string representing a regular expression.
       
   144         # urlconf_name is a string representing the module containing URLconfs.
       
   145         self.regex = re.compile(regex, re.UNICODE)
       
   146         self.urlconf_name = urlconf_name
       
   147         if not isinstance(urlconf_name, basestring):
       
   148             self._urlconf_module = self.urlconf_name
       
   149         self.callback = None
       
   150         self.default_kwargs = default_kwargs or {}
       
   151         self.namespace = namespace
       
   152         self.app_name = app_name
       
   153         self._reverse_dict = None
       
   154         self._namespace_dict = None
       
   155         self._app_dict = None
       
   156 
       
   157     def __repr__(self):
       
   158         return '<%s %s (%s:%s) %s>' % (self.__class__.__name__, self.urlconf_name, self.app_name, self.namespace, self.regex.pattern)
       
   159 
       
   160     def _populate(self):
       
   161         lookups = MultiValueDict()
       
   162         namespaces = {}
       
   163         apps = {}
       
   164         for pattern in reversed(self.url_patterns):
       
   165             p_pattern = pattern.regex.pattern
       
   166             if p_pattern.startswith('^'):
       
   167                 p_pattern = p_pattern[1:]
       
   168             if isinstance(pattern, RegexURLResolver):
       
   169                 if pattern.namespace:
       
   170                     namespaces[pattern.namespace] = (p_pattern, pattern)
       
   171                     if pattern.app_name:
       
   172                         apps.setdefault(pattern.app_name, []).append(pattern.namespace)
       
   173                 else:
       
   174                     parent = normalize(pattern.regex.pattern)
       
   175                     for name in pattern.reverse_dict:
       
   176                         for matches, pat in pattern.reverse_dict.getlist(name):
       
   177                             new_matches = []
       
   178                             for piece, p_args in parent:
       
   179                                 new_matches.extend([(piece + suffix, p_args + args) for (suffix, args) in matches])
       
   180                             lookups.appendlist(name, (new_matches, p_pattern + pat))
       
   181                     for namespace, (prefix, sub_pattern) in pattern.namespace_dict.items():
       
   182                         namespaces[namespace] = (p_pattern + prefix, sub_pattern)
       
   183                     for app_name, namespace_list in pattern.app_dict.items():
       
   184                         apps.setdefault(app_name, []).extend(namespace_list)
       
   185             else:
       
   186                 bits = normalize(p_pattern)
       
   187                 lookups.appendlist(pattern.callback, (bits, p_pattern))
       
   188                 lookups.appendlist(pattern.name, (bits, p_pattern))
       
   189         self._reverse_dict = lookups
       
   190         self._namespace_dict = namespaces
       
   191         self._app_dict = apps
       
   192 
       
   193     def _get_reverse_dict(self):
       
   194         if self._reverse_dict is None:
       
   195             self._populate()
       
   196         return self._reverse_dict
       
   197     reverse_dict = property(_get_reverse_dict)
       
   198 
       
   199     def _get_namespace_dict(self):
       
   200         if self._namespace_dict is None:
       
   201             self._populate()
       
   202         return self._namespace_dict
       
   203     namespace_dict = property(_get_namespace_dict)
       
   204 
       
   205     def _get_app_dict(self):
       
   206         if self._app_dict is None:
       
   207             self._populate()
       
   208         return self._app_dict
       
   209     app_dict = property(_get_app_dict)
       
   210 
       
   211     def resolve(self, path):
       
   212         tried = []
       
   213         match = self.regex.search(path)
       
   214         if match:
       
   215             new_path = path[match.end():]
       
   216             for pattern in self.url_patterns:
       
   217                 try:
       
   218                     sub_match = pattern.resolve(new_path)
       
   219                 except Resolver404, e:
       
   220                     sub_tried = e.args[0].get('tried')
       
   221                     if sub_tried is not None:
       
   222                         tried.extend([(pattern.regex.pattern + '   ' + t) for t in sub_tried])
       
   223                     else:
       
   224                         tried.append(pattern.regex.pattern)
       
   225                 else:
       
   226                     if sub_match:
       
   227                         sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()])
       
   228                         sub_match_dict.update(self.default_kwargs)
       
   229                         for k, v in sub_match[2].iteritems():
       
   230                             sub_match_dict[smart_str(k)] = v
       
   231                         return sub_match[0], sub_match[1], sub_match_dict
       
   232                     tried.append(pattern.regex.pattern)
       
   233             raise Resolver404, {'tried': tried, 'path': new_path}
       
   234         raise Resolver404, {'path' : path}
       
   235 
       
   236     def _get_urlconf_module(self):
       
   237         try:
       
   238             return self._urlconf_module
       
   239         except AttributeError:
       
   240             self._urlconf_module = import_module(self.urlconf_name)
       
   241             return self._urlconf_module
       
   242     urlconf_module = property(_get_urlconf_module)
       
   243 
       
   244     def _get_url_patterns(self):
       
   245         patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
       
   246         try:
       
   247             iter(patterns)
       
   248         except TypeError:
       
   249             raise ImproperlyConfigured("The included urlconf %s doesn't have any "
       
   250                 "patterns in it" % self.urlconf_name)
       
   251         return patterns
       
   252     url_patterns = property(_get_url_patterns)
       
   253 
       
   254     def _resolve_special(self, view_type):
       
   255         callback = getattr(self.urlconf_module, 'handler%s' % view_type)
       
   256         mod_name, func_name = get_mod_func(callback)
       
   257         try:
       
   258             return getattr(import_module(mod_name), func_name), {}
       
   259         except (ImportError, AttributeError), e:
       
   260             raise ViewDoesNotExist, "Tried %s. Error was: %s" % (callback, str(e))
       
   261 
       
   262     def resolve404(self):
       
   263         return self._resolve_special('404')
       
   264 
       
   265     def resolve500(self):
       
   266         return self._resolve_special('500')
       
   267 
       
   268     def reverse(self, lookup_view, *args, **kwargs):
       
   269         if args and kwargs:
       
   270             raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
       
   271         try:
       
   272             lookup_view = get_callable(lookup_view, True)
       
   273         except (ImportError, AttributeError), e:
       
   274             raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e))
       
   275         possibilities = self.reverse_dict.getlist(lookup_view)
       
   276         for possibility, pattern in possibilities:
       
   277             for result, params in possibility:
       
   278                 if args:
       
   279                     if len(args) != len(params):
       
   280                         continue
       
   281                     unicode_args = [force_unicode(val) for val in args]
       
   282                     candidate =  result % dict(zip(params, unicode_args))
       
   283                 else:
       
   284                     if set(kwargs.keys()) != set(params):
       
   285                         continue
       
   286                     unicode_kwargs = dict([(k, force_unicode(v)) for (k, v) in kwargs.items()])
       
   287                     candidate = result % unicode_kwargs
       
   288                 if re.search(u'^%s' % pattern, candidate, re.UNICODE):
       
   289                     return candidate
       
   290         # lookup_view can be URL label, or dotted path, or callable, Any of
       
   291         # these can be passed in at the top, but callables are not friendly in
       
   292         # error messages.
       
   293         m = getattr(lookup_view, '__module__', None)
       
   294         n = getattr(lookup_view, '__name__', None)
       
   295         if m is not None and n is not None:
       
   296             lookup_view_s = "%s.%s" % (m, n)
       
   297         else:
       
   298             lookup_view_s = lookup_view
       
   299         raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword "
       
   300                 "arguments '%s' not found." % (lookup_view_s, args, kwargs))
       
   301 
       
   302 def resolve(path, urlconf=None):
       
   303     return get_resolver(urlconf).resolve(path)
       
   304 
       
   305 def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None):
       
   306     resolver = get_resolver(urlconf)
       
   307     args = args or []
       
   308     kwargs = kwargs or {}
       
   309 
       
   310     if prefix is None:
       
   311         prefix = get_script_prefix()
       
   312 
       
   313     if not isinstance(viewname, basestring):
       
   314         view = viewname
       
   315     else:
       
   316         parts = viewname.split(':')
       
   317         parts.reverse()
       
   318         view = parts[0]
       
   319         path = parts[1:]
       
   320 
       
   321         resolved_path = []
       
   322         while path:
       
   323             ns = path.pop()
       
   324 
       
   325             # Lookup the name to see if it could be an app identifier
       
   326             try:
       
   327                 app_list = resolver.app_dict[ns]
       
   328                 # Yes! Path part matches an app in the current Resolver
       
   329                 if current_app and current_app in app_list:
       
   330                     # If we are reversing for a particular app, use that namespace
       
   331                     ns = current_app
       
   332                 elif ns not in app_list:
       
   333                     # The name isn't shared by one of the instances (i.e., the default)
       
   334                     # so just pick the first instance as the default.
       
   335                     ns = app_list[0]
       
   336             except KeyError:
       
   337                 pass
       
   338 
       
   339             try:
       
   340                 extra, resolver = resolver.namespace_dict[ns]
       
   341                 resolved_path.append(ns)
       
   342                 prefix = prefix + extra
       
   343             except KeyError, key:
       
   344                 if resolved_path:
       
   345                     raise NoReverseMatch("%s is not a registered namespace inside '%s'" % (key, ':'.join(resolved_path)))
       
   346                 else:
       
   347                     raise NoReverseMatch("%s is not a registered namespace" % key)
       
   348 
       
   349     return iri_to_uri(u'%s%s' % (prefix, resolver.reverse(view,
       
   350             *args, **kwargs)))
       
   351 
       
   352 def clear_url_caches():
       
   353     global _resolver_cache
       
   354     global _callable_cache
       
   355     _resolver_cache.clear()
       
   356     _callable_cache.clear()
       
   357 
       
   358 def set_script_prefix(prefix):
       
   359     """
       
   360     Sets the script prefix for the current thread.
       
   361     """
       
   362     if not prefix.endswith('/'):
       
   363         prefix += '/'
       
   364     _prefixes[currentThread()] = prefix
       
   365 
       
   366 def get_script_prefix():
       
   367     """
       
   368     Returns the currently active script prefix. Useful for client code that
       
   369     wishes to construct their own URLs manually (although accessing the request
       
   370     instance is normally going to be a lot cleaner).
       
   371     """
       
   372     return _prefixes.get(currentThread(), u'/')
       
   373