|
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 |