App Engine Python SDK version 1.9.13
[gae.git] / python / google / appengine / _internal / django / core / urlresolvers.py
blobf0c280ee0f3c5860b2b525e934c9e50a8fe9d88b
1 """
2 This module converts requested URLs to callback view functions.
4 RegexURLResolver is the main class here. Its resolve() method takes a URL (as
5 a string) and returns a tuple in this format:
7 (view_function, function_args, function_kwargs)
8 """
10 import re
12 from google.appengine._internal.django.http import Http404
13 from google.appengine._internal.django.conf import settings
14 from google.appengine._internal.django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
15 from google.appengine._internal.django.utils.datastructures import MultiValueDict
16 from google.appengine._internal.django.utils.encoding import iri_to_uri, force_unicode, smart_str
17 from google.appengine._internal.django.utils.functional import memoize
18 from google.appengine._internal.django.utils.importlib import import_module
19 from google.appengine._internal.django.utils.regex_helper import normalize
20 from google.appengine._internal.django.utils.thread_support import currentThread
22 _resolver_cache = {} # Maps URLconf modules to RegexURLResolver instances.
23 _callable_cache = {} # Maps view and url pattern names to their view functions.
25 # SCRIPT_NAME prefixes for each thread are stored here. If there's no entry for
26 # the current thread (which is the only one we ever access), it is assumed to
27 # be empty.
28 _prefixes = {}
30 # Overridden URLconfs for each thread are stored here.
31 _urlconfs = {}
33 class Resolver404(Http404):
34 pass
36 class NoReverseMatch(Exception):
37 # Don't make this raise an error when used in a template.
38 silent_variable_failure = True
40 def get_callable(lookup_view, can_fail=False):
41 """
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.
49 """
50 if not callable(lookup_view):
51 try:
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)
55 if func_name != '':
56 lookup_view = getattr(import_module(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):
60 if not can_fail:
61 raise
62 except UnicodeEncodeError:
63 pass
64 return lookup_view
65 get_callable = memoize(get_callable, _callable_cache, 1)
67 def get_resolver(urlconf):
68 if urlconf is None:
69 from google.appengine._internal.django.conf import settings
70 urlconf = settings.ROOT_URLCONF
71 return RegexURLResolver(r'^/', urlconf)
72 get_resolver = memoize(get_resolver, _resolver_cache, 1)
74 def get_mod_func(callback):
75 # Converts 'django.views.news.stories.story_detail' to
76 # ['django.views.news.stories', 'story_detail']
77 try:
78 dot = callback.rindex('.')
79 except ValueError:
80 return callback, ''
81 return callback[:dot], callback[dot+1:]
83 class RegexURLPattern(object):
84 def __init__(self, regex, callback, default_args=None, name=None):
85 # regex is a string representing a regular expression.
86 # callback is either a string like 'foo.views.news.stories.story_detail'
87 # which represents the path to a module and a view function name, or a
88 # callable object (view).
89 self.regex = re.compile(regex, re.UNICODE)
90 if callable(callback):
91 self._callback = callback
92 else:
93 self._callback = None
94 self._callback_str = callback
95 self.default_args = default_args or {}
96 self.name = name
98 def __repr__(self):
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'):
106 return
107 self._callback_str = prefix + '.' + self._callback_str
109 def resolve(self, path):
110 match = self.regex.search(path)
111 if match:
112 # If there are any named groups, use those as kwargs, ignoring
113 # non-named groups. Otherwise, pass all non-named arguments as
114 # positional arguments.
115 kwargs = match.groupdict()
116 if kwargs:
117 args = ()
118 else:
119 args = match.groups()
120 # In both cases, pass any extra_kwargs as **kwargs.
121 kwargs.update(self.default_args)
123 return self.callback, args, kwargs
125 def _get_callback(self):
126 if self._callback is not None:
127 return self._callback
128 try:
129 self._callback = get_callable(self._callback_str)
130 except ImportError, e:
131 mod_name, _ = get_mod_func(self._callback_str)
132 raise ViewDoesNotExist("Could not import %s. Error was: %s" % (mod_name, str(e)))
133 except AttributeError, e:
134 mod_name, func_name = get_mod_func(self._callback_str)
135 raise ViewDoesNotExist("Tried %s in module %s. Error was: %s" % (func_name, mod_name, str(e)))
136 return self._callback
137 callback = property(_get_callback)
139 class RegexURLResolver(object):
140 def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
141 # regex is a string representing a regular expression.
142 # urlconf_name is a string representing the module containing URLconfs.
143 self.regex = re.compile(regex, re.UNICODE)
144 self.urlconf_name = urlconf_name
145 if not isinstance(urlconf_name, basestring):
146 self._urlconf_module = self.urlconf_name
147 self.callback = None
148 self.default_kwargs = default_kwargs or {}
149 self.namespace = namespace
150 self.app_name = app_name
151 self._reverse_dict = None
152 self._namespace_dict = None
153 self._app_dict = None
155 def __repr__(self):
156 return '<%s %s (%s:%s) %s>' % (self.__class__.__name__, self.urlconf_name, self.app_name, self.namespace, self.regex.pattern)
158 def _populate(self):
159 lookups = MultiValueDict()
160 namespaces = {}
161 apps = {}
162 for pattern in reversed(self.url_patterns):
163 p_pattern = pattern.regex.pattern
164 if p_pattern.startswith('^'):
165 p_pattern = p_pattern[1:]
166 if isinstance(pattern, RegexURLResolver):
167 if pattern.namespace:
168 namespaces[pattern.namespace] = (p_pattern, pattern)
169 if pattern.app_name:
170 apps.setdefault(pattern.app_name, []).append(pattern.namespace)
171 else:
172 parent = normalize(pattern.regex.pattern)
173 for name in pattern.reverse_dict:
174 for matches, pat in pattern.reverse_dict.getlist(name):
175 new_matches = []
176 for piece, p_args in parent:
177 new_matches.extend([(piece + suffix, p_args + args) for (suffix, args) in matches])
178 lookups.appendlist(name, (new_matches, p_pattern + pat))
179 for namespace, (prefix, sub_pattern) in pattern.namespace_dict.items():
180 namespaces[namespace] = (p_pattern + prefix, sub_pattern)
181 for app_name, namespace_list in pattern.app_dict.items():
182 apps.setdefault(app_name, []).extend(namespace_list)
183 else:
184 bits = normalize(p_pattern)
185 lookups.appendlist(pattern.callback, (bits, p_pattern))
186 if pattern.name is not None:
187 lookups.appendlist(pattern.name, (bits, p_pattern))
188 self._reverse_dict = lookups
189 self._namespace_dict = namespaces
190 self._app_dict = apps
192 def _get_reverse_dict(self):
193 if self._reverse_dict is None:
194 self._populate()
195 return self._reverse_dict
196 reverse_dict = property(_get_reverse_dict)
198 def _get_namespace_dict(self):
199 if self._namespace_dict is None:
200 self._populate()
201 return self._namespace_dict
202 namespace_dict = property(_get_namespace_dict)
204 def _get_app_dict(self):
205 if self._app_dict is None:
206 self._populate()
207 return self._app_dict
208 app_dict = property(_get_app_dict)
210 def resolve(self, path):
211 tried = []
212 match = self.regex.search(path)
213 if match:
214 new_path = path[match.end():]
215 for pattern in self.url_patterns:
216 try:
217 sub_match = pattern.resolve(new_path)
218 except Resolver404, e:
219 sub_tried = e.args[0].get('tried')
220 if sub_tried is not None:
221 tried.extend([(pattern.regex.pattern + ' ' + t) for t in sub_tried])
222 else:
223 tried.append(pattern.regex.pattern)
224 else:
225 if sub_match:
226 sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()])
227 sub_match_dict.update(self.default_kwargs)
228 for k, v in sub_match[2].iteritems():
229 sub_match_dict[smart_str(k)] = v
230 return sub_match[0], sub_match[1], sub_match_dict
231 tried.append(pattern.regex.pattern)
232 raise Resolver404({'tried': tried, 'path': new_path})
233 raise Resolver404({'path' : path})
235 def _get_urlconf_module(self):
236 try:
237 return self._urlconf_module
238 except AttributeError:
239 self._urlconf_module = import_module(self.urlconf_name)
240 return self._urlconf_module
241 urlconf_module = property(_get_urlconf_module)
243 def _get_url_patterns(self):
244 patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
245 try:
246 iter(patterns)
247 except TypeError:
248 raise ImproperlyConfigured("The included urlconf %s doesn't have any patterns in it" % self.urlconf_name)
249 return patterns
250 url_patterns = property(_get_url_patterns)
252 def _resolve_special(self, view_type):
253 callback = getattr(self.urlconf_module, 'handler%s' % view_type)
254 try:
255 return get_callable(callback), {}
256 except (ImportError, AttributeError), e:
257 raise ViewDoesNotExist("Tried %s. Error was: %s" % (callback, str(e)))
259 def resolve404(self):
260 return self._resolve_special('404')
262 def resolve500(self):
263 return self._resolve_special('500')
265 def reverse(self, lookup_view, *args, **kwargs):
266 if args and kwargs:
267 raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
268 try:
269 lookup_view = get_callable(lookup_view, True)
270 except (ImportError, AttributeError), e:
271 raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e))
272 possibilities = self.reverse_dict.getlist(lookup_view)
273 for possibility, pattern in possibilities:
274 for result, params in possibility:
275 if args:
276 if len(args) != len(params):
277 continue
278 unicode_args = [force_unicode(val) for val in args]
279 candidate = result % dict(zip(params, unicode_args))
280 else:
281 if set(kwargs.keys()) != set(params):
282 continue
283 unicode_kwargs = dict([(k, force_unicode(v)) for (k, v) in kwargs.items()])
284 candidate = result % unicode_kwargs
285 if re.search(u'^%s' % pattern, candidate, re.UNICODE):
286 return candidate
287 # lookup_view can be URL label, or dotted path, or callable, Any of
288 # these can be passed in at the top, but callables are not friendly in
289 # error messages.
290 m = getattr(lookup_view, '__module__', None)
291 n = getattr(lookup_view, '__name__', None)
292 if m is not None and n is not None:
293 lookup_view_s = "%s.%s" % (m, n)
294 else:
295 lookup_view_s = lookup_view
296 raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword "
297 "arguments '%s' not found." % (lookup_view_s, args, kwargs))
299 def resolve(path, urlconf=None):
300 if urlconf is None:
301 urlconf = get_urlconf()
302 return get_resolver(urlconf).resolve(path)
304 def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None):
305 if urlconf is None:
306 urlconf = get_urlconf()
307 resolver = get_resolver(urlconf)
308 args = args or []
309 kwargs = kwargs or {}
311 if prefix is None:
312 prefix = get_script_prefix()
314 if not isinstance(viewname, basestring):
315 view = viewname
316 else:
317 parts = viewname.split(':')
318 parts.reverse()
319 view = parts[0]
320 path = parts[1:]
322 resolved_path = []
323 while path:
324 ns = path.pop()
326 # Lookup the name to see if it could be an app identifier
327 try:
328 app_list = resolver.app_dict[ns]
329 # Yes! Path part matches an app in the current Resolver
330 if current_app and current_app in app_list:
331 # If we are reversing for a particular app, use that namespace
332 ns = current_app
333 elif ns not in app_list:
334 # The name isn't shared by one of the instances (i.e., the default)
335 # so just pick the first instance as the default.
336 ns = app_list[0]
337 except KeyError:
338 pass
340 try:
341 extra, resolver = resolver.namespace_dict[ns]
342 resolved_path.append(ns)
343 prefix = prefix + extra
344 except KeyError, key:
345 if resolved_path:
346 raise NoReverseMatch("%s is not a registered namespace inside '%s'" % (key, ':'.join(resolved_path)))
347 else:
348 raise NoReverseMatch("%s is not a registered namespace" % key)
350 return iri_to_uri(u'%s%s' % (prefix, resolver.reverse(view,
351 *args, **kwargs)))
353 def clear_url_caches():
354 global _resolver_cache
355 global _callable_cache
356 _resolver_cache.clear()
357 _callable_cache.clear()
359 def set_script_prefix(prefix):
361 Sets the script prefix for the current thread.
363 if not prefix.endswith('/'):
364 prefix += '/'
365 _prefixes[currentThread()] = prefix
367 def get_script_prefix():
369 Returns the currently active script prefix. Useful for client code that
370 wishes to construct their own URLs manually (although accessing the request
371 instance is normally going to be a lot cleaner).
373 return _prefixes.get(currentThread(), u'/')
375 def set_urlconf(urlconf_name):
377 Sets the URLconf for the current thread (overriding the default one in
378 settings). Set to None to revert back to the default.
380 thread = currentThread()
381 if urlconf_name:
382 _urlconfs[thread] = urlconf_name
383 else:
384 # faster than wrapping in a try/except
385 if thread in _urlconfs:
386 del _urlconfs[thread]
388 def get_urlconf(default=None):
390 Returns the root URLconf to use for the current thread if it has been
391 changed from the default one.
393 thread = currentThread()
394 if thread in _urlconfs:
395 return _urlconfs[thread]
396 return default