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