Ticket #4566: urlresolvers.py

File urlresolvers.py, 10.8 KB (added by anonymous, 17 years ago)

urlresolvers.py with described speedups

Line 
1"""
2This module converts requested URLs to callback view functions.
3
4RegexURLResolver is the main class here. Its resolve() method takes a URL (as
5a string) and returns a tuple in this format:
6
7 (view_function, function_args, function_kwargs)
8"""
9
10from django.http import Http404
11from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
12import re
13
14def cacheable(func, cache):
15 def wrapper(arg):
16 if arg in cache:
17 return cache[arg]
18
19 result = func(arg)
20 cache[arg] = result
21 return result
22 return wrapper
23
24resolver_cache = {}
25
26def get_resolver(urlconf):
27 if urlconf is None:
28 from django.conf import settings
29 urlconf = settings.ROOT_URLCONF
30 return RegexURLResolver(r'^/', urlconf)
31get_resolver = cacheable(get_resolver, resolver_cache)
32
33class Resolver404(Http404):
34 pass
35
36class NoReverseMatch(Exception):
37 # Don't make this raise an error when used in a template.
38 silent_variable_failure = True
39
40def get_mod_func(callback):
41 # Converts 'django.views.news.stories.story_detail' to
42 # ['django.views.news.stories', 'story_detail']
43 try:
44 dot = callback.rindex('.')
45 except ValueError:
46 return callback, ''
47 return callback[:dot], callback[dot+1:]
48
49def reverse_helper(regex, *args, **kwargs):
50 """
51 Does a "reverse" lookup -- returns the URL for the given args/kwargs.
52 The args/kwargs are applied to the given compiled regular expression.
53 For example:
54
55 >>> reverse_helper(re.compile('^places/(\d+)/$'), 3)
56 'places/3/'
57 >>> reverse_helper(re.compile('^places/(?P<id>\d+)/$'), id=3)
58 'places/3/'
59 >>> reverse_helper(re.compile('^people/(?P<state>\w\w)/(\w+)/$'), 'adrian', state='il')
60 'people/il/adrian/'
61
62 Raises NoReverseMatch if the args/kwargs aren't valid for the regex.
63 """
64 # TODO: Handle nested parenthesis in the following regex.
65 result = re.sub(r'\(([^)]+)\)', MatchChecker(args, kwargs), regex.pattern)
66 return result.replace('^', '').replace('$', '')
67
68class MatchChecker(object):
69 "Class used in reverse RegexURLPattern lookup."
70 def __init__(self, args, kwargs):
71 self.args, self.kwargs = args, kwargs
72 self.current_arg = 0
73
74 def __call__(self, match_obj):
75 # match_obj.group(1) is the contents of the parenthesis.
76 # First we need to figure out whether it's a named or unnamed group.
77 #
78 grouped = match_obj.group(1)
79 m = re.search(r'^\?P<(\w+)>(.*?)$', grouped)
80 if m: # If this was a named group...
81 # m.group(1) is the name of the group
82 # m.group(2) is the regex.
83 try:
84 value = self.kwargs[m.group(1)]
85 except KeyError:
86 # It was a named group, but the arg was passed in as a
87 # positional arg or not at all.
88 try:
89 value = self.args[self.current_arg]
90 self.current_arg += 1
91 except IndexError:
92 # The arg wasn't passed in.
93 raise NoReverseMatch('Not enough positional arguments passed in')
94 test_regex = m.group(2)
95 else: # Otherwise, this was a positional (unnamed) group.
96 try:
97 value = self.args[self.current_arg]
98 self.current_arg += 1
99 except IndexError:
100 # The arg wasn't passed in.
101 raise NoReverseMatch('Not enough positional arguments passed in')
102 test_regex = grouped
103 # Note we're using re.match here on purpose because the start of
104 # to string needs to match.
105 if not re.match(test_regex + '$', str(value)): # TODO: Unicode?
106 raise NoReverseMatch("Value %r didn't match regular expression %r" % (value, test_regex))
107 return str(value) # TODO: Unicode?
108
109class RegexURLPattern(object):
110 def __init__(self, regex, callback, default_args=None, name=None):
111 # regex is a string representing a regular expression.
112 # callback is either a string like 'foo.views.news.stories.story_detail'
113 # which represents the path to a module and a view function name, or a
114 # callable object (view).
115 self.regex = re.compile(regex)
116 if callable(callback):
117 self._callback = callback
118 else:
119 self._callback = None
120 self._callback_str = callback
121 self.default_args = default_args or {}
122 self.name = name
123
124 def add_prefix(self, prefix):
125 """
126 Adds the prefix string to a string-based callback.
127 """
128 if not prefix or not hasattr(self, '_callback_str'):
129 return
130 self._callback_str = prefix + '.' + self._callback_str
131
132 def resolve(self, path):
133 match = self.regex.search(path)
134 if match:
135 # If there are any named groups, use those as kwargs, ignoring
136 # non-named groups. Otherwise, pass all non-named arguments as
137 # positional arguments.
138 kwargs = match.groupdict()
139 if kwargs:
140 args = ()
141 else:
142 args = match.groups()
143 # In both cases, pass any extra_kwargs as **kwargs.
144 kwargs.update(self.default_args)
145
146 return self.callback, args, kwargs
147
148 def _get_callback(self):
149 if self._callback is not None:
150 return self._callback
151 mod_name, func_name = get_mod_func(self._callback_str)
152 try:
153 self._callback = getattr(__import__(mod_name, {}, {}, ['']), func_name)
154 except ImportError, e:
155 raise ViewDoesNotExist, "Could not import %s. Error was: %s" % (mod_name, str(e))
156 except AttributeError, e:
157 raise ViewDoesNotExist, "Tried %s in module %s. Error was: %s" % (func_name, mod_name, str(e))
158 return self._callback
159 callback = property(_get_callback)
160
161 def reverse(self, viewname, *args, **kwargs):
162 mod_name, func_name = get_mod_func(viewname)
163 try:
164 lookup_view = getattr(__import__(mod_name, {}, {}, ['']), func_name)
165 except (ImportError, AttributeError):
166 raise NoReverseMatch
167 if lookup_view != self.callback:
168 raise NoReverseMatch
169 return self.reverse_helper(*args, **kwargs)
170
171 def reverse_helper(self, *args, **kwargs):
172 return reverse_helper(self.regex, *args, **kwargs)
173
174class RegexURLResolver(object):
175 def __init__(self, regex, urlconf_name, default_kwargs=None):
176 # regex is a string representing a regular expression.
177 # urlconf_name is a string representing the module containing urlconfs.
178 self.regex = re.compile(regex)
179 self.urlconf_name = urlconf_name
180 self.callback = None
181 self.default_kwargs = default_kwargs or {}
182 self.reverse_dict = {}
183
184 for pattern in reversed(self.urlconf_module.urlpatterns):
185 if isinstance(pattern, RegexURLResolver):
186 for key, value in pattern.reverse_dict.iteritems():
187 self.reverse_dict[key] = (pattern,) + value
188 else:
189 self.reverse_dict[pattern.callback] = (pattern,)
190 self.reverse_dict[pattern.name] = (pattern,)
191
192 def resolve(self, path):
193 tried = []
194 match = self.regex.search(path)
195 if match:
196 new_path = path[match.end():]
197 for pattern in self.urlconf_module.urlpatterns:
198 try:
199 sub_match = pattern.resolve(new_path)
200 except Resolver404, e:
201 tried.extend([(pattern.regex.pattern + ' ' + t) for t in e.args[0]['tried']])
202 else:
203 if sub_match:
204 sub_match_dict = dict(self.default_kwargs, **sub_match[2])
205 return sub_match[0], sub_match[1], dict(match.groupdict(), **sub_match_dict)
206 tried.append(pattern.regex.pattern)
207 raise Resolver404, {'tried': tried, 'path': new_path}
208
209 def _get_urlconf_module(self):
210 try:
211 return self._urlconf_module
212 except AttributeError:
213 try:
214 self._urlconf_module = __import__(self.urlconf_name, {}, {}, [''])
215 except ValueError, e:
216 # Invalid urlconf_name, such as "foo.bar." (note trailing period)
217 raise ImproperlyConfigured, "Error while importing URLconf %r: %s" % (self.urlconf_name, e)
218 return self._urlconf_module
219 urlconf_module = property(_get_urlconf_module)
220
221 def _get_url_patterns(self):
222 return self.urlconf_module.urlpatterns
223 url_patterns = property(_get_url_patterns)
224
225 def _resolve_special(self, view_type):
226 callback = getattr(self.urlconf_module, 'handler%s' % view_type)
227 mod_name, func_name = get_mod_func(callback)
228 try:
229 return getattr(__import__(mod_name, {}, {}, ['']), func_name), {}
230 except (ImportError, AttributeError), e:
231 raise ViewDoesNotExist, "Tried %s. Error was: %s" % (callback, str(e))
232
233 def resolve404(self):
234 return self._resolve_special('404')
235
236 def resolve500(self):
237 return self._resolve_special('500')
238
239 def reverse(self, lookup_view, *args, **kwargs):
240 lookup_view = make_callable(lookup_view)
241 if lookup_view in self.reverse_dict:
242 url_parts = (reverse_helper(part.regex, *args, **kwargs) for part in self.reverse_dict[lookup_view])
243 return ''.join(url_parts)
244 raise NoReverseMatch
245
246 def reverse_helper(self, lookup_view, *args, **kwargs):
247 sub_match = self.reverse(lookup_view, *args, **kwargs)
248 result = reverse_helper(self.regex, *args, **kwargs)
249 return result + sub_match
250
251callable_cache = {}
252def make_callable(lookup_view):
253 if not callable(lookup_view):
254 mod_name, func_name = get_mod_func(lookup_view)
255 if func_name != '':
256 try:
257 lookup_view = getattr(__import__(mod_name, {}, {}, ['']), func_name)
258 except (ImportError, AttributeError):
259 if func_name != '':
260 raise NoReverseMatch
261 return lookup_view
262make_callable = cacheable(make_callable, callable_cache)
263
264def resolve(path, urlconf=None):
265 #if urlconf is None:
266 # from django.conf import settings
267 # urlconf = settings.ROOT_URLCONF
268 #resolver = RegexURLResolver(r'^/', urlconf)
269 resolver = get_resolver(urlconf)
270 return resolver.resolve(path)
271
272def reverse(viewname, urlconf=None, args=None, kwargs=None):
273 args = args or []
274 kwargs = kwargs or {}
275 resolver = get_resolver(urlconf)
276 return '/' + resolver.reverse(viewname, *args, **kwargs)
Back to Top