Ticket #5090: urlresolvers.py

File urlresolvers.py, 12.1 KB (added by Alex Nickolaenkov <nickolaenkov@…>, 17 years ago)

simplies patch

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