7
7
(view_function, function_args, function_kwargs)
10
12
from django.http import Http404
11
13
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
14
from django.utils.encoding import iri_to_uri, force_unicode, smart_str
15
from django.utils.functional import memoize
16
from django.utils.regex_helper import normalize
17
from django.utils.thread_support import currentThread
22
from django.utils.itercompat import reversed # Python 2.3 fallback
23
from sets import Set as set
25
_resolver_cache = {} # Maps urlconf modules to RegexURLResolver instances.
26
_callable_cache = {} # Maps view and url pattern names to their view functions.
28
# SCRIPT_NAME prefixes for each thread are stored here. If there's no entry for
29
# the current thread (which is the only one we ever access), it is assumed to
14
33
class Resolver404(Http404):
18
37
# Don't make this raise an error when used in a template.
19
38
silent_variable_failure = True
40
def get_callable(lookup_view, can_fail=False):
42
Convert a string version of a function name to the callable object.
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.
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.
50
if not callable(lookup_view):
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)
56
lookup_view = getattr(__import__(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):
62
except UnicodeEncodeError:
65
get_callable = memoize(get_callable, _callable_cache, 1)
67
def get_resolver(urlconf):
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)
21
74
def get_mod_func(callback):
22
75
# Converts 'django.views.news.stories.story_detail' to
23
76
# ['django.views.news.stories', 'story_detail']
27
80
return callback, ''
28
81
return callback[:dot], callback[dot+1:]
30
def reverse_helper(regex, *args, **kwargs):
32
Does a "reverse" lookup -- returns the URL for the given args/kwargs.
33
The args/kwargs are applied to the given compiled regular expression.
36
>>> reverse_helper(re.compile('^places/(\d+)/$'), 3)
38
>>> reverse_helper(re.compile('^places/(?P<id>\d+)/$'), id=3)
40
>>> reverse_helper(re.compile('^people/(?P<state>\w\w)/(\w+)/$'), 'adrian', state='il')
43
Raises NoReverseMatch if the args/kwargs aren't valid for the regex.
45
# TODO: Handle nested parenthesis in the following regex.
46
result = re.sub(r'\(([^)]+)\)', MatchChecker(args, kwargs), regex.pattern)
47
return result.replace('^', '').replace('$', '')
49
class MatchChecker(object):
50
"Class used in reverse RegexURLPattern lookup."
51
def __init__(self, args, kwargs):
52
self.args, self.kwargs = args, kwargs
55
def __call__(self, match_obj):
56
# match_obj.group(1) is the contents of the parenthesis.
57
# First we need to figure out whether it's a named or unnamed group.
59
grouped = match_obj.group(1)
60
m = re.search(r'^\?P<(\w+)>(.*?)$', grouped)
61
if m: # If this was a named group...
62
# m.group(1) is the name of the group
63
# m.group(2) is the regex.
65
value = self.kwargs[m.group(1)]
67
# It was a named group, but the arg was passed in as a
68
# positional arg or not at all.
70
value = self.args[self.current_arg]
73
# The arg wasn't passed in.
74
raise NoReverseMatch('Not enough positional arguments passed in')
75
test_regex = m.group(2)
76
else: # Otherwise, this was a positional (unnamed) group.
78
value = self.args[self.current_arg]
81
# The arg wasn't passed in.
82
raise NoReverseMatch('Not enough positional arguments passed in')
84
# Note we're using re.match here on purpose because the start of
85
# to string needs to match.
86
if not re.match(test_regex + '$', str(value)): # TODO: Unicode?
87
raise NoReverseMatch("Value %r didn't match regular expression %r" % (value, test_regex))
88
return str(value) # TODO: Unicode?
90
83
class RegexURLPattern(object):
91
def __init__(self, regex, callback, default_args=None):
84
def __init__(self, regex, callback, default_args=None, name=None):
92
85
# regex is a string representing a regular expression.
93
86
# callback is either a string like 'foo.views.news.stories.story_detail'
94
87
# which represents the path to a module and a view function name, or a
95
88
# callable object (view).
96
self.regex = re.compile(regex)
89
self.regex = re.compile(regex, re.UNICODE)
97
90
if callable(callback):
98
91
self._callback = callback
100
93
self._callback = None
101
94
self._callback_str = callback
102
95
self.default_args = default_args or {}
99
return '<%s %s %s>' % (self.__class__.__name__, self.name, self.regex.pattern)
101
def add_prefix(self, prefix):
103
Adds the prefix string to a string-based callback.
105
if not prefix or not hasattr(self, '_callback_str'):
107
self._callback_str = prefix + '.' + self._callback_str
104
109
def resolve(self, path):
105
110
match = self.regex.search(path)
120
125
def _get_callback(self):
121
126
if self._callback is not None:
122
127
return self._callback
123
mod_name, func_name = get_mod_func(self._callback_str)
125
self._callback = getattr(__import__(mod_name, {}, {}, ['']), func_name)
129
self._callback = get_callable(self._callback_str)
126
130
except ImportError, e:
131
mod_name, _ = get_mod_func(self._callback_str)
127
132
raise ViewDoesNotExist, "Could not import %s. Error was: %s" % (mod_name, str(e))
128
133
except AttributeError, e:
134
mod_name, func_name = get_mod_func(self._callback_str)
129
135
raise ViewDoesNotExist, "Tried %s in module %s. Error was: %s" % (func_name, mod_name, str(e))
130
136
return self._callback
131
137
callback = property(_get_callback)
133
def reverse(self, viewname, *args, **kwargs):
134
mod_name, func_name = get_mod_func(viewname)
136
lookup_view = getattr(__import__(mod_name, {}, {}, ['']), func_name)
137
except (ImportError, AttributeError):
139
if lookup_view != self.callback:
141
return self.reverse_helper(*args, **kwargs)
143
def reverse_helper(self, *args, **kwargs):
144
return reverse_helper(self.regex, *args, **kwargs)
146
139
class RegexURLResolver(object):
147
140
def __init__(self, regex, urlconf_name, default_kwargs=None):
148
141
# regex is a string representing a regular expression.
149
142
# urlconf_name is a string representing the module containing urlconfs.
150
self.regex = re.compile(regex)
143
self.regex = re.compile(regex, re.UNICODE)
151
144
self.urlconf_name = urlconf_name
152
145
self.callback = None
153
146
self.default_kwargs = default_kwargs or {}
147
self._reverse_dict = {}
150
return '<%s %s %s>' % (self.__class__.__name__, self.urlconf_name, self.regex.pattern)
152
def _get_reverse_dict(self):
153
if not self._reverse_dict and hasattr(self.urlconf_module, 'urlpatterns'):
154
for pattern in reversed(self.urlconf_module.urlpatterns):
155
p_pattern = pattern.regex.pattern
156
if p_pattern.startswith('^'):
157
p_pattern = p_pattern[1:]
158
if isinstance(pattern, RegexURLResolver):
159
parent = normalize(pattern.regex.pattern)
160
for name, (matches, pat) in pattern.reverse_dict.iteritems():
162
for piece, p_args in parent:
163
new_matches.extend([(piece + suffix, p_args + args)
164
for (suffix, args) in matches])
165
self._reverse_dict[name] = new_matches, p_pattern + pat
167
bits = normalize(p_pattern)
168
self._reverse_dict[pattern.callback] = bits, p_pattern
169
self._reverse_dict[pattern.name] = bits, p_pattern
170
return self._reverse_dict
171
reverse_dict = property(_get_reverse_dict)
155
173
def resolve(self, path):
164
182
tried.extend([(pattern.regex.pattern + ' ' + t) for t in e.args[0]['tried']])
167
sub_match_dict = dict(self.default_kwargs, **sub_match[2])
168
return sub_match[0], sub_match[1], dict(match.groupdict(), **sub_match_dict)
185
sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()])
186
sub_match_dict.update(self.default_kwargs)
187
for k, v in sub_match[2].iteritems():
188
sub_match_dict[smart_str(k)] = v
189
return sub_match[0], sub_match[1], sub_match_dict
169
190
tried.append(pattern.regex.pattern)
170
191
raise Resolver404, {'tried': tried, 'path': new_path}
200
217
return self._resolve_special('500')
202
219
def reverse(self, lookup_view, *args, **kwargs):
203
if not callable(lookup_view):
204
mod_name, func_name = get_mod_func(lookup_view)
206
lookup_view = getattr(__import__(mod_name, {}, {}, ['']), func_name)
207
except (ImportError, AttributeError):
209
for pattern in self.urlconf_module.urlpatterns:
210
if isinstance(pattern, RegexURLResolver):
212
return pattern.reverse_helper(lookup_view, *args, **kwargs)
213
except NoReverseMatch:
215
elif pattern.callback == lookup_view:
217
return pattern.reverse_helper(*args, **kwargs)
218
except NoReverseMatch:
222
def reverse_helper(self, lookup_view, *args, **kwargs):
223
sub_match = self.reverse(lookup_view, *args, **kwargs)
224
result = reverse_helper(self.regex, *args, **kwargs)
225
return result + sub_match
221
raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
223
lookup_view = get_callable(lookup_view, True)
224
except (ImportError, AttributeError), e:
225
raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e))
226
possibilities, pattern = self.reverse_dict.get(lookup_view, [(), ()])
227
for result, params in possibilities:
229
if len(args) != len(params):
231
unicode_args = [force_unicode(val) for val in args]
232
candidate = result % dict(zip(params, unicode_args))
234
if set(kwargs.keys()) != set(params):
236
unicode_kwargs = dict([(k, force_unicode(v)) for (k, v) in kwargs.items()])
237
candidate = result % unicode_kwargs
238
if re.search(u'^%s' % pattern, candidate, re.UNICODE):
240
raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword "
241
"arguments '%s' not found." % (lookup_view, args, kwargs))
227
243
def resolve(path, urlconf=None):
229
from django.conf import settings
230
urlconf = settings.ROOT_URLCONF
231
resolver = RegexURLResolver(r'^/', urlconf)
232
return resolver.resolve(path)
244
return get_resolver(urlconf).resolve(path)
234
def reverse(viewname, urlconf=None, args=None, kwargs=None):
246
def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None):
235
247
args = args or []
236
248
kwargs = kwargs or {}
238
from django.conf import settings
239
urlconf = settings.ROOT_URLCONF
240
resolver = RegexURLResolver(r'^/', urlconf)
241
return '/' + resolver.reverse(viewname, *args, **kwargs)
250
prefix = get_script_prefix()
251
return iri_to_uri(u'%s%s' % (prefix, get_resolver(urlconf).reverse(viewname,
254
def clear_url_caches():
255
global _resolver_cache
256
global _callable_cache
257
_resolver_cache.clear()
258
_callable_cache.clear()
260
def set_script_prefix(prefix):
262
Sets the script prefix for the current thread.
264
if not prefix.endswith('/'):
266
_prefixes[currentThread()] = prefix
268
def get_script_prefix():
270
Returns the currently active script prefix. Useful for client code that
271
wishes to construct their own URLs manually (although accessing the request
272
instance is normally going to be a lot cleaner).
274
return _prefixes.get(currentThread(), u'/')