Django

Code

Ticket #4566: urlresolvers.py

File urlresolvers.py, 10.8 kB (added by anonymous, 1 year ago)

urlresolvers.py with described speedups

Line 
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 from django.http import Http404
11 from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
12 import re
13
14 def 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
24 resolver_cache = {}
25
26 def get_resolver(urlconf):
27     if urlconf is None:
28         from django.conf import settings
29         urlconf = settings.ROOT_URLCONF
30     return RegexURLResolver(r'^/', urlconf)
31 get_resolver = cacheable(get_resolver, resolver_cache)
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_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
49 def 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
68 class 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
109 class 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
174 class 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
251 callable_cache = {}
252 def 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
262 make_callable = cacheable(make_callable, callable_cache)
263
264 def 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
272 def 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)