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