Django

Code

root/django/trunk/django/core/urlresolvers.py

Revision 9099, 10.9 kB (checked in by mtredinnick, 2 months ago)

Fixed another case of reverse URL resolving that wasn't working.

This is a similar situation to that fixed in r9087. We weren't merging
multiple levels of include() calls together correctly.

  • Property svn:eol-style set to native
  • Property svn:keywords set to LastChangedRevision
Line 
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.regex_helper import normalize
18 from django.utils.thread_support import currentThread
19
20 try:
21     reversed
22 except NameError:
23     from django.utils.itercompat import reversed     # Python 2.3 fallback
24     from sets import Set as set
25
26 _resolver_cache = {} # Maps urlconf modules to RegexURLResolver instances.
27 _callable_cache = {} # Maps view and url pattern names to their view functions.
28
29 # SCRIPT_NAME prefixes for each thread are stored here. If there's no entry for
30 # the current thread (which is the only one we ever access), it is assumed to
31 # be empty.
32 _prefixes = {}
33
34 class Resolver404(Http404):
35     pass
36
37 class NoReverseMatch(Exception):
38     # Don't make this raise an error when used in a template.
39     silent_variable_failure = True
40
41 def get_callable(lookup_view, can_fail=False):
42     """
43     Convert a string version of a function name to the callable object.
44
45     If the lookup_view is not an import path, it is assumed to be a URL pattern
46     label and the original string is returned.
47
48     If can_fail is True, lookup_view might be a URL pattern label, so errors
49     during the import fail and the string is returned.
50     """
51     if not callable(lookup_view):
52         try:
53             # Bail early for non-ASCII strings (they can't be functions).
54             lookup_view = lookup_view.encode('ascii')
55             mod_name, func_name = get_mod_func(lookup_view)
56             if func_name != '':
57                 lookup_view = getattr(__import__(mod_name, {}, {}, ['']), func_name)
58                 if not callable(lookup_view):
59                     raise AttributeError("'%s.%s' is not a callable." % (mod_name, func_name))
60         except (ImportError, AttributeError):
61             if not can_fail:
62                 raise
63         except UnicodeEncodeError:
64             pass
65     return lookup_view
66 get_callable = memoize(get_callable, _callable_cache, 1)
67
68 def get_resolver(urlconf):
69     if urlconf is None:
70         from django.conf import settings
71         urlconf = settings.ROOT_URLCONF
72     return RegexURLResolver(r'^/', urlconf)
73 get_resolver = memoize(get_resolver, _resolver_cache, 1)
74
75 def get_mod_func(callback):
76     # Converts 'django.views.news.stories.story_detail' to
77     # ['django.views.news.stories', 'story_detail']
78     try:
79         dot = callback.rindex('.')
80     except ValueError:
81         return callback, ''
82     return callback[:dot], callback[dot+1:]
83
84 class RegexURLPattern(object):
85     def __init__(self, regex, callback, default_args=None, name=None):
86         # regex is a string representing a regular expression.
87         # callback is either a string like 'foo.views.news.stories.story_detail'
88         # which represents the path to a module and a view function name, or a
89         # callable object (view).
90         self.regex = re.compile(regex, re.UNICODE)
91         if callable(callback):
92             self._callback = callback
93         else:
94             self._callback = None
95             self._callback_str = callback
96         self.default_args = default_args or {}
97         self.name = name
98
99     def __repr__(self):
100         return '<%s %s %s>' % (self.__class__.__name__, self.name, self.regex.pattern)
101
102     def add_prefix(self, prefix):
103         """
104         Adds the prefix string to a string-based callback.
105         """
106         if not prefix or not hasattr(self, '_callback_str'):
107             return
108         self._callback_str = prefix + '.' + self._callback_str
109
110     def resolve(self, path):
111         match = self.regex.search(path)
112         if match:
113             # If there are any named groups, use those as kwargs, ignoring
114             # non-named groups. Otherwise, pass all non-named arguments as
115             # positional arguments.
116             kwargs = match.groupdict()
117             if kwargs:
118                 args = ()
119             else:
120                 args = match.groups()
121             # In both cases, pass any extra_kwargs as **kwargs.
122             kwargs.update(self.default_args)
123
124             return self.callback, args, kwargs
125
126     def _get_callback(self):
127         if self._callback is not None:
128             return self._callback
129         try:
130             self._callback = get_callable(self._callback_str)
131         except ImportError, e:
132             mod_name, _ = get_mod_func(self._callback_str)
133             raise ViewDoesNotExist, "Could not import %s. Error was: %s" % (mod_name, str(e))
134         except AttributeError, e:
135             mod_name, func_name = get_mod_func(self._callback_str)
136             raise ViewDoesNotExist, "Tried %s in module %s. Error was: %s" % (func_name, mod_name, str(e))
137         return self._callback
138     callback = property(_get_callback)
139
140 class RegexURLResolver(object):
141     def __init__(self, regex, urlconf_name, default_kwargs=None):
142         # regex is a string representing a regular expression.
143         # urlconf_name is a string representing the module containing urlconfs.
144         self.regex = re.compile(regex, re.UNICODE)
145         self.urlconf_name = urlconf_name
146         self.callback = None
147         self.default_kwargs = default_kwargs or {}
148         self._reverse_dict = MultiValueDict()
149
150     def __repr__(self):
151         return '<%s %s %s>' % (self.__class__.__name__, self.urlconf_name, self.regex.pattern)
152
153     def _get_reverse_dict(self):
154         if not self._reverse_dict and hasattr(self.urlconf_module, 'urlpatterns'):
155             for pattern in reversed(self.urlconf_module.urlpatterns):
156                 p_pattern = pattern.regex.pattern
157                 if p_pattern.startswith('^'):
158                     p_pattern = p_pattern[1:]
159                 if isinstance(pattern, RegexURLResolver):
160                     parent = normalize(pattern.regex.pattern)
161                     for name in pattern.reverse_dict:
162                         for matches, pat in pattern.reverse_dict.getlist(name):
163                             new_matches = []
164                             for piece, p_args in parent:
165                                 new_matches.extend([(piece + suffix, p_args + args) for (suffix, args) in matches])
166                             self._reverse_dict.appendlist(name, (new_matches, p_pattern + pat))
167                 else:
168                     bits = normalize(p_pattern)
169                     self._reverse_dict.appendlist(pattern.callback, (bits, p_pattern))
170                     self._reverse_dict.appendlist(pattern.name, (bits, p_pattern))
171         return self._reverse_dict
172     reverse_dict = property(_get_reverse_dict)
173
174     def resolve(self, path):
175         tried = []
176         match = self.regex.search(path)
177         if match:
178             new_path = path[match.end():]
179             for pattern in self.urlconf_module.urlpatterns:
180                 try:
181                     sub_match = pattern.resolve(new_path)
182                 except Resolver404, e:
183                     tried.extend([(pattern.regex.pattern + '   ' + t) for t in e.args[0]['tried']])
184                 else:
185                     if sub_match:
186                         sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()])
187                         sub_match_dict.update(self.default_kwargs)
188                         for k, v in sub_match[2].iteritems():
189                             sub_match_dict[smart_str(k)] = v
190                         return sub_match[0], sub_match[1], sub_match_dict
191                     tried.append(pattern.regex.pattern)
192             raise Resolver404, {'tried': tried, 'path': new_path}
193
194     def _get_urlconf_module(self):
195         try:
196             return self._urlconf_module
197         except AttributeError:
198             self._urlconf_module = __import__(self.urlconf_name, {}, {}, [''])
199             return self._urlconf_module
200     urlconf_module = property(_get_urlconf_module)
201
202     def _get_url_patterns(self):
203         return self.urlconf_module.urlpatterns
204     url_patterns = property(_get_url_patterns)
205
206     def _resolve_special(self, view_type):
207         callback = getattr(self.urlconf_module, 'handler%s' % view_type)
208         mod_name, func_name = get_mod_func(callback)
209         try:
210             return getattr(__import__(mod_name, {}, {}, ['']), func_name), {}
211         except (ImportError, AttributeError), e:
212             raise ViewDoesNotExist, "Tried %s. Error was: %s" % (callback, str(e))
213
214     def resolve404(self):
215         return self._resolve_special('404')
216
217     def resolve500(self):
218         return self._resolve_special('500')
219
220     def reverse(self, lookup_view, *args, **kwargs):
221         if args and kwargs:
222             raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
223         try:
224             lookup_view = get_callable(lookup_view, True)
225         except (ImportError, AttributeError), e:
226             raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e))
227         possibilities = self.reverse_dict.getlist(lookup_view)
228         for possibility, pattern in possibilities:
229             for result, params in possibility:
230                 if args:
231                     if len(args) != len(params):
232                         continue
233                     unicode_args = [force_unicode(val) for val in args]
234                     candidate =  result % dict(zip(params, unicode_args))
235                 else:
236                     if set(kwargs.keys()) != set(params):
237                         continue
238                     unicode_kwargs = dict([(k, force_unicode(v)) for (k, v) in kwargs.items()])
239                     candidate = result % unicode_kwargs
240                 if re.search(u'^%s' % pattern, candidate, re.UNICODE):
241                     return candidate
242         raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword "
243                 "arguments '%s' not found." % (lookup_view, args, kwargs))
244
245 def resolve(path, urlconf=None):
246     return get_resolver(urlconf).resolve(path)
247
248 def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None):
249     args = args or []
250     kwargs = kwargs or {}
251     if prefix is None:
252         prefix = get_script_prefix()
253     return iri_to_uri(u'%s%s' % (prefix, get_resolver(urlconf).reverse(viewname,
254             *args, **kwargs)))
255
256 def clear_url_caches():
257     global _resolver_cache
258     global _callable_cache
259     _resolver_cache.clear()
260     _callable_cache.clear()
261
262 def set_script_prefix(prefix):
263     """
264     Sets the script prefix for the current thread.
265     """
266     if not prefix.endswith('/'):
267         prefix += '/'
268     _prefixes[currentThread()] = prefix
269
270 def get_script_prefix():
271     """
272     Returns the currently active script prefix. Useful for client code that
273     wishes to construct their own URLs manually (although accessing the request
274     instance is normally going to be a lot cleaner).
275     """
276     return _prefixes.get(currentThread(), u'/')
Note: See TracBrowser for help on using the browser.