web/lib/django/core/urlresolvers.py
changeset 38 77b6da96e6f1
equal deleted inserted replaced
37:8d941af65caf 38:77b6da96e6f1
       
     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.conf import settings
       
    14 from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
       
    15 from django.utils.datastructures import MultiValueDict
       
    16 from django.utils.encoding import iri_to_uri, force_unicode, smart_str
       
    17 from django.utils.functional import memoize
       
    18 from django.utils.importlib import import_module
       
    19 from django.utils.regex_helper import normalize
       
    20 from django.utils.thread_support import currentThread
       
    21 
       
    22 _resolver_cache = {} # Maps URLconf modules to RegexURLResolver instances.
       
    23 _callable_cache = {} # Maps view and url pattern names to their view functions.
       
    24 
       
    25 # SCRIPT_NAME prefixes for each thread are stored here. If there's no entry for
       
    26 # the current thread (which is the only one we ever access), it is assumed to
       
    27 # be empty.
       
    28 _prefixes = {}
       
    29 
       
    30 # Overridden URLconfs for each thread are stored here.
       
    31 _urlconfs = {}
       
    32 
       
    33 class Resolver404(Http404):
       
    34     pass
       
    35 
       
    36 class NoReverseMatch(Exception):
       
    37     # Don't make this raise an error when used in a template.
       
    38     silent_variable_failure = True
       
    39 
       
    40 def get_callable(lookup_view, can_fail=False):
       
    41     """
       
    42     Convert a string version of a function name to the callable object.
       
    43 
       
    44     If the lookup_view is not an import path, it is assumed to be a URL pattern
       
    45     label and the original string is returned.
       
    46 
       
    47     If can_fail is True, lookup_view might be a URL pattern label, so errors
       
    48     during the import fail and the string is returned.
       
    49     """
       
    50     if not callable(lookup_view):
       
    51         try:
       
    52             # Bail early for non-ASCII strings (they can't be functions).
       
    53             lookup_view = lookup_view.encode('ascii')
       
    54             mod_name, func_name = get_mod_func(lookup_view)
       
    55             if func_name != '':
       
    56                 lookup_view = getattr(import_module(mod_name), func_name)
       
    57                 if not callable(lookup_view):
       
    58                     raise AttributeError("'%s.%s' is not a callable." % (mod_name, func_name))
       
    59         except (ImportError, AttributeError):
       
    60             if not can_fail:
       
    61                 raise
       
    62         except UnicodeEncodeError:
       
    63             pass
       
    64     return lookup_view
       
    65 get_callable = memoize(get_callable, _callable_cache, 1)
       
    66 
       
    67 def get_resolver(urlconf):
       
    68     if urlconf is None:
       
    69         from django.conf import settings
       
    70         urlconf = settings.ROOT_URLCONF
       
    71     return RegexURLResolver(r'^/', urlconf)
       
    72 get_resolver = memoize(get_resolver, _resolver_cache, 1)
       
    73 
       
    74 def get_mod_func(callback):
       
    75     # Converts 'django.views.news.stories.story_detail' to
       
    76     # ['django.views.news.stories', 'story_detail']
       
    77     try:
       
    78         dot = callback.rindex('.')
       
    79     except ValueError:
       
    80         return callback, ''
       
    81     return callback[:dot], callback[dot+1:]
       
    82 
       
    83 class RegexURLPattern(object):
       
    84     def __init__(self, regex, callback, default_args=None, name=None):
       
    85         # regex is a string representing a regular expression.
       
    86         # callback is either a string like 'foo.views.news.stories.story_detail'
       
    87         # which represents the path to a module and a view function name, or a
       
    88         # callable object (view).
       
    89         self.regex = re.compile(regex, re.UNICODE)
       
    90         if callable(callback):
       
    91             self._callback = callback
       
    92         else:
       
    93             self._callback = None
       
    94             self._callback_str = callback
       
    95         self.default_args = default_args or {}
       
    96         self.name = name
       
    97 
       
    98     def __repr__(self):
       
    99         return '<%s %s %s>' % (self.__class__.__name__, self.name, self.regex.pattern)
       
   100 
       
   101     def add_prefix(self, prefix):
       
   102         """
       
   103         Adds the prefix string to a string-based callback.
       
   104         """
       
   105         if not prefix or not hasattr(self, '_callback_str'):
       
   106             return
       
   107         self._callback_str = prefix + '.' + self._callback_str
       
   108 
       
   109     def resolve(self, path):
       
   110         match = self.regex.search(path)
       
   111         if match:
       
   112             # If there are any named groups, use those as kwargs, ignoring
       
   113             # non-named groups. Otherwise, pass all non-named arguments as
       
   114             # positional arguments.
       
   115             kwargs = match.groupdict()
       
   116             if kwargs:
       
   117                 args = ()
       
   118             else:
       
   119                 args = match.groups()
       
   120             # In both cases, pass any extra_kwargs as **kwargs.
       
   121             kwargs.update(self.default_args)
       
   122 
       
   123             return self.callback, args, kwargs
       
   124 
       
   125     def _get_callback(self):
       
   126         if self._callback is not None:
       
   127             return self._callback
       
   128         try:
       
   129             self._callback = get_callable(self._callback_str)
       
   130         except ImportError, e:
       
   131             mod_name, _ = get_mod_func(self._callback_str)
       
   132             raise ViewDoesNotExist("Could not import %s. Error was: %s" % (mod_name, str(e)))
       
   133         except AttributeError, e:
       
   134             mod_name, func_name = get_mod_func(self._callback_str)
       
   135             raise ViewDoesNotExist("Tried %s in module %s. Error was: %s" % (func_name, mod_name, str(e)))
       
   136         return self._callback
       
   137     callback = property(_get_callback)
       
   138 
       
   139 class RegexURLResolver(object):
       
   140     def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
       
   141         # regex is a string representing a regular expression.
       
   142         # urlconf_name is a string representing the module containing URLconfs.
       
   143         self.regex = re.compile(regex, re.UNICODE)
       
   144         self.urlconf_name = urlconf_name
       
   145         if not isinstance(urlconf_name, basestring):
       
   146             self._urlconf_module = self.urlconf_name
       
   147         self.callback = None
       
   148         self.default_kwargs = default_kwargs or {}
       
   149         self.namespace = namespace
       
   150         self.app_name = app_name
       
   151         self._reverse_dict = None
       
   152         self._namespace_dict = None
       
   153         self._app_dict = None
       
   154 
       
   155     def __repr__(self):
       
   156         return '<%s %s (%s:%s) %s>' % (self.__class__.__name__, self.urlconf_name, self.app_name, self.namespace, self.regex.pattern)
       
   157 
       
   158     def _populate(self):
       
   159         lookups = MultiValueDict()
       
   160         namespaces = {}
       
   161         apps = {}
       
   162         for pattern in reversed(self.url_patterns):
       
   163             p_pattern = pattern.regex.pattern
       
   164             if p_pattern.startswith('^'):
       
   165                 p_pattern = p_pattern[1:]
       
   166             if isinstance(pattern, RegexURLResolver):
       
   167                 if pattern.namespace:
       
   168                     namespaces[pattern.namespace] = (p_pattern, pattern)
       
   169                     if pattern.app_name:
       
   170                         apps.setdefault(pattern.app_name, []).append(pattern.namespace)
       
   171                 else:
       
   172                     parent = normalize(pattern.regex.pattern)
       
   173                     for name in pattern.reverse_dict:
       
   174                         for matches, pat in pattern.reverse_dict.getlist(name):
       
   175                             new_matches = []
       
   176                             for piece, p_args in parent:
       
   177                                 new_matches.extend([(piece + suffix, p_args + args) for (suffix, args) in matches])
       
   178                             lookups.appendlist(name, (new_matches, p_pattern + pat))
       
   179                     for namespace, (prefix, sub_pattern) in pattern.namespace_dict.items():
       
   180                         namespaces[namespace] = (p_pattern + prefix, sub_pattern)
       
   181                     for app_name, namespace_list in pattern.app_dict.items():
       
   182                         apps.setdefault(app_name, []).extend(namespace_list)
       
   183             else:
       
   184                 bits = normalize(p_pattern)
       
   185                 lookups.appendlist(pattern.callback, (bits, p_pattern))
       
   186                 lookups.appendlist(pattern.name, (bits, p_pattern))
       
   187         self._reverse_dict = lookups
       
   188         self._namespace_dict = namespaces
       
   189         self._app_dict = apps
       
   190 
       
   191     def _get_reverse_dict(self):
       
   192         if self._reverse_dict is None:
       
   193             self._populate()
       
   194         return self._reverse_dict
       
   195     reverse_dict = property(_get_reverse_dict)
       
   196 
       
   197     def _get_namespace_dict(self):
       
   198         if self._namespace_dict is None:
       
   199             self._populate()
       
   200         return self._namespace_dict
       
   201     namespace_dict = property(_get_namespace_dict)
       
   202 
       
   203     def _get_app_dict(self):
       
   204         if self._app_dict is None:
       
   205             self._populate()
       
   206         return self._app_dict
       
   207     app_dict = property(_get_app_dict)
       
   208 
       
   209     def resolve(self, path):
       
   210         tried = []
       
   211         match = self.regex.search(path)
       
   212         if match:
       
   213             new_path = path[match.end():]
       
   214             for pattern in self.url_patterns:
       
   215                 try:
       
   216                     sub_match = pattern.resolve(new_path)
       
   217                 except Resolver404, e:
       
   218                     sub_tried = e.args[0].get('tried')
       
   219                     if sub_tried is not None:
       
   220                         tried.extend([(pattern.regex.pattern + '   ' + t) for t in sub_tried])
       
   221                     else:
       
   222                         tried.append(pattern.regex.pattern)
       
   223                 else:
       
   224                     if sub_match:
       
   225                         sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()])
       
   226                         sub_match_dict.update(self.default_kwargs)
       
   227                         for k, v in sub_match[2].iteritems():
       
   228                             sub_match_dict[smart_str(k)] = v
       
   229                         return sub_match[0], sub_match[1], sub_match_dict
       
   230                     tried.append(pattern.regex.pattern)
       
   231             raise Resolver404({'tried': tried, 'path': new_path})
       
   232         raise Resolver404({'path' : path})
       
   233 
       
   234     def _get_urlconf_module(self):
       
   235         try:
       
   236             return self._urlconf_module
       
   237         except AttributeError:
       
   238             self._urlconf_module = import_module(self.urlconf_name)
       
   239             return self._urlconf_module
       
   240     urlconf_module = property(_get_urlconf_module)
       
   241 
       
   242     def _get_url_patterns(self):
       
   243         patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
       
   244         try:
       
   245             iter(patterns)
       
   246         except TypeError:
       
   247             raise ImproperlyConfigured("The included urlconf %s doesn't have any patterns in it" % self.urlconf_name)
       
   248         return patterns
       
   249     url_patterns = property(_get_url_patterns)
       
   250 
       
   251     def _resolve_special(self, view_type):
       
   252         callback = getattr(self.urlconf_module, 'handler%s' % view_type)
       
   253         try:
       
   254             return get_callable(callback), {}
       
   255         except (ImportError, AttributeError), e:
       
   256             raise ViewDoesNotExist("Tried %s. Error was: %s" % (callback, str(e)))
       
   257 
       
   258     def resolve404(self):
       
   259         return self._resolve_special('404')
       
   260 
       
   261     def resolve500(self):
       
   262         return self._resolve_special('500')
       
   263 
       
   264     def reverse(self, lookup_view, *args, **kwargs):
       
   265         if args and kwargs:
       
   266             raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
       
   267         try:
       
   268             lookup_view = get_callable(lookup_view, True)
       
   269         except (ImportError, AttributeError), e:
       
   270             raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e))
       
   271         possibilities = self.reverse_dict.getlist(lookup_view)
       
   272         for possibility, pattern in possibilities:
       
   273             for result, params in possibility:
       
   274                 if args:
       
   275                     if len(args) != len(params):
       
   276                         continue
       
   277                     unicode_args = [force_unicode(val) for val in args]
       
   278                     candidate =  result % dict(zip(params, unicode_args))
       
   279                 else:
       
   280                     if set(kwargs.keys()) != set(params):
       
   281                         continue
       
   282                     unicode_kwargs = dict([(k, force_unicode(v)) for (k, v) in kwargs.items()])
       
   283                     candidate = result % unicode_kwargs
       
   284                 if re.search(u'^%s' % pattern, candidate, re.UNICODE):
       
   285                     return candidate
       
   286         # lookup_view can be URL label, or dotted path, or callable, Any of
       
   287         # these can be passed in at the top, but callables are not friendly in
       
   288         # error messages.
       
   289         m = getattr(lookup_view, '__module__', None)
       
   290         n = getattr(lookup_view, '__name__', None)
       
   291         if m is not None and n is not None:
       
   292             lookup_view_s = "%s.%s" % (m, n)
       
   293         else:
       
   294             lookup_view_s = lookup_view
       
   295         raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword "
       
   296                 "arguments '%s' not found." % (lookup_view_s, args, kwargs))
       
   297 
       
   298 def resolve(path, urlconf=None):
       
   299     if urlconf is None:
       
   300         urlconf = get_urlconf()
       
   301     return get_resolver(urlconf).resolve(path)
       
   302 
       
   303 def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None):
       
   304     if urlconf is None:
       
   305         urlconf = get_urlconf()
       
   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 
       
   374 def set_urlconf(urlconf_name):
       
   375     """
       
   376     Sets the URLconf for the current thread (overriding the default one in
       
   377     settings). Set to None to revert back to the default.
       
   378     """
       
   379     thread = currentThread()
       
   380     if urlconf_name:
       
   381         _urlconfs[thread] = urlconf_name
       
   382     else:
       
   383         # faster than wrapping in a try/except
       
   384         if thread in _urlconfs:
       
   385             del _urlconfs[thread]
       
   386 
       
   387 def get_urlconf(default=None):
       
   388     """
       
   389     Returns the root URLconf to use for the current thread if it has been
       
   390     changed from the default one.
       
   391     """
       
   392     thread = currentThread()
       
   393     if thread in _urlconfs:
       
   394         return _urlconfs[thread]
       
   395     return default