Django

Code

root/django/branches/gis/django/core/urlresolvers.py

Revision 8215, 12.8 kB (checked in by jbronn, 4 months ago)

gis: Merged revisions 7981-8001,8003-8011,8013-8033,8035-8036,8038-8039,8041-8063,8065-8076,8078-8139,8141-8154,8156-8214 via svnmerge from trunk.

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