Django

Code

root/django/trunk/django/core/urlresolvers.py

Revision 11155, 11.5 kB (checked in by russellm, 2 days ago)

Fixed #10834 -- Corrected [11120] to ensure that there is a difference between catching a bad URL pattern and an new (no URLs) project. Thanks to Matt Welch for the report.

  • 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.datastructures import MultiValueDict
15 from django.utils.encoding import iri_to_uri, force_unicode, smart_str
16 from django.utils.functional import memoize
17 from django.utils.importlib import import_module
18 from django.utils.regex_helper import normalize
19 from django.utils.thread_support import currentThread
20
21 try:
22     reversed
23 except NameError:
24     from django.utils.itercompat import reversed     # Python 2.3 fallback
25     from sets import Set as set
26
27 _resolver_cache = {} # Maps URLconf modules to RegexURLResolver instances.
28 _callable_cache = {} # Maps view and url pattern names to their view functions.
29
30 # SCRIPT_NAME prefixes for each thread are stored here. If there's no entry for
31 # the current thread (which is the only one we ever access), it is assumed to
32 # be empty.
33 _prefixes = {}
34
35 class Resolver404(Http404):
36     pass
37
38 class NoReverseMatch(Exception):
39     # Don't make this raise an error when used in a template.
40     silent_variable_failure = True
41
42 def get_callable(lookup_view, can_fail=False):
43     """
44     Convert a string version of a function name to the callable object.
45
46     If the lookup_view is not an import path, it is assumed to be a URL pattern
47     label and the original string is returned.
48
49     If can_fail is True, lookup_view might be a URL pattern label, so errors
50     during the import fail and the string is returned.
51     """
52     if not callable(lookup_view):
53         try:
54             # Bail early for non-ASCII strings (they can't be functions).
55             lookup_view = lookup_view.encode('ascii')
56             mod_name, func_name = get_mod_func(lookup_view)
57             if func_name != '':
58                 lookup_view = getattr(import_module(mod_name), func_name)
59                 if not callable(lookup_view):
60                     raise AttributeError("'%s.%s' is not a callable." % (mod_name, func_name))
61         except (ImportError, AttributeError):
62             if not can_fail:
63                 raise
64         except UnicodeEncodeError:
65             pass
66     return lookup_view
67 get_callable = memoize(get_callable, _callable_cache, 1)
68
69 def get_resolver(urlconf):
70     if urlconf is None:
71         from django.conf import settings
72         urlconf = settings.ROOT_URLCONF
73     return RegexURLResolver(r'^/', urlconf)
74 get_resolver = memoize(get_resolver, _resolver_cache, 1)
75
76 def get_mod_func(callback):
77     # Converts 'django.views.news.stories.story_detail' to
78     # ['django.views.news.stories', 'story_detail']
79     try:
80         dot = callback.rindex('.')
81     except ValueError:
82         return callback, ''
83     return callback[:dot], callback[dot+1:]
84
85 class RegexURLPattern(object):
86     def __init__(self, regex, callback, default_args=None, name=None):
87         # regex is a string representing a regular expression.
88         # callback is either a string like 'foo.views.news.stories.story_detail'
89         # which represents the path to a module and a view function name, or a
90         # callable object (view).
91         self.regex = re.compile(regex, re.UNICODE)
92         if callable(callback):
93             self._callback = callback
94         else:
95             self._callback = None
96             self._callback_str = callback
97         self.default_args = default_args or {}
98         self.name = name
99
100     def __repr__(self):
101         return '<%s %s %s>' % (self.__class__.__name__, self.name, self.regex.pattern)
102
103     def add_prefix(self, prefix):
104         """
105         Adds the prefix string to a string-based callback.
106         """
107         if not prefix or not hasattr(self, '_callback_str'):
108             return
109         self._callback_str = prefix + '.' + self._callback_str
110
111     def resolve(self, path):
112         match = self.regex.search(path)
113         if match:
114             # If there are any named groups, use those as kwargs, ignoring
115             # non-named groups. Otherwise, pass all non-named arguments as
116             # positional arguments.
117             kwargs = match.groupdict()
118             if kwargs:
119                 args = ()
120             else:
121                 args = match.groups()
122             # In both cases, pass any extra_kwargs as **kwargs.
123             kwargs.update(self.default_args)
124
125             return self.callback, args, kwargs
126
127     def _get_callback(self):
128         if self._callback is not None:
129             return self._callback
130         try:
131             self._callback = get_callable(self._callback_str)
132         except ImportError, e:
133             mod_name, _ = get_mod_func(self._callback_str)
134             raise ViewDoesNotExist, "Could not import %s. Error was: %s" % (mod_name, str(e))
135         except AttributeError, e:
136             mod_name, func_name = get_mod_func(self._callback_str)
137             raise ViewDoesNotExist, "Tried %s in module %s. Error was: %s" % (func_name, mod_name, str(e))
138         return self._callback
139     callback = property(_get_callback)
140
141 class RegexURLResolver(object):
142     def __init__(self, regex, urlconf_name, default_kwargs=None):
143         # regex is a string representing a regular expression.
144         # urlconf_name is a string representing the module containing URLconfs.
145         self.regex = re.compile(regex, re.UNICODE)
146         self.urlconf_name = urlconf_name
147         if not isinstance(urlconf_name, basestring):
148             self._urlconf_module = self.urlconf_name
149         self.callback = None
150         self.default_kwargs = default_kwargs or {}
151         self._reverse_dict = MultiValueDict()
152
153     def __repr__(self):
154         return '<%s %s %s>' % (self.__class__.__name__, self.urlconf_name, self.regex.pattern)
155
156     def _get_reverse_dict(self):
157         if not self._reverse_dict:
158             lookups = MultiValueDict()
159             for pattern in reversed(self.url_patterns):
160                 p_pattern = pattern.regex.pattern
161                 if p_pattern.startswith('^'):
162                     p_pattern = p_pattern[1:]
163                 if isinstance(pattern, RegexURLResolver):
164                     parent = normalize(pattern.regex.pattern)
165                     for name in pattern.reverse_dict:
166                         for matches, pat in pattern.reverse_dict.getlist(name):
167                             new_matches = []
168                             for piece, p_args in parent:
169                                 new_matches.extend([(piece + suffix, p_args + args) for (suffix, args) in matches])
170                             lookups.appendlist(name, (new_matches, p_pattern + pat))
171                 else:
172                     bits = normalize(p_pattern)
173                     lookups.appendlist(pattern.callback, (bits, p_pattern))
174                     lookups.appendlist(pattern.name, (bits, p_pattern))
175             self._reverse_dict = lookups
176         return self._reverse_dict
177     reverse_dict = property(_get_reverse_dict)
178
179     def resolve(self, path):
180         tried = []
181         match = self.regex.search(path)
182         if match:
183             new_path = path[match.end():]
184             for pattern in self.url_patterns:
185                 try:
186                     sub_match = pattern.resolve(new_path)
187                 except Resolver404, e:
188                     sub_tried = e.args[0].get('tried')
189                     if sub_tried is not None:
190                         tried.extend([(pattern.regex.pattern + '   ' + t) for t in sub_tried])
191                     else:
192                         tried.append(pattern.regex.pattern)
193                 else:
194                     if sub_match:
195                         sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()])
196                         sub_match_dict.update(self.default_kwargs)
197                         for k, v in sub_match[2].iteritems():
198                             sub_match_dict[smart_str(k)] = v
199                         return sub_match[0], sub_match[1], sub_match_dict
200                     tried.append(pattern.regex.pattern)
201             raise Resolver404, {'tried': tried, 'path': new_path}
202         raise Resolver404, {'path' : path}
203
204     def _get_urlconf_module(self):
205         try:
206             return self._urlconf_module
207         except AttributeError:
208             self._urlconf_module = import_module(self.urlconf_name)
209             return self._urlconf_module
210     urlconf_module = property(_get_urlconf_module)
211
212     def _get_url_patterns(self):
213         patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
214         try:
215             iter(patterns)
216         except TypeError:
217             raise ImproperlyConfigured("The included urlconf %s doesn't have any "
218                 "patterns in it" % self.urlconf_name)
219         return patterns
220     url_patterns = property(_get_url_patterns)
221
222     def _resolve_special(self, view_type):
223         callback = getattr(self.urlconf_module, 'handler%s' % view_type)
224         mod_name, func_name = get_mod_func(callback)
225         try:
226             return getattr(import_module(mod_name), func_name), {}
227         except (ImportError, AttributeError), e:
228             raise ViewDoesNotExist, "Tried %s. Error was: %s" % (callback, str(e))
229
230     def resolve404(self):
231         return self._resolve_special('404')
232
233     def resolve500(self):
234         return self._resolve_special('500')
235
236     def reverse(self, lookup_view, *args, **kwargs):
237         if args and kwargs:
238             raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
239         try:
240             lookup_view = get_callable(lookup_view, True)
241         except (ImportError, AttributeError), e:
242             raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e))
243         possibilities = self.reverse_dict.getlist(lookup_view)
244         for possibility, pattern in possibilities:
245             for result, params in possibility:
246                 if args:
247                     if len(args) != len(params):
248                         continue
249                     unicode_args = [force_unicode(val) for val in args]
250                     candidate =  result % dict(zip(params, unicode_args))
251                 else:
252                     if set(kwargs.keys()) != set(params):
253                         continue
254                     unicode_kwargs = dict([(k, force_unicode(v)) for (k, v) in kwargs.items()])
255                     candidate = result % unicode_kwargs
256                 if re.search(u'^%s' % pattern, candidate, re.UNICODE):
257                     return candidate
258         raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword "
259                 "arguments '%s' not found." % (lookup_view, args, kwargs))
260
261 def resolve(path, urlconf=None):
262     return get_resolver(urlconf).resolve(path)
263
264 def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None):
265     args = args or []
266     kwargs = kwargs or {}
267     if prefix is None:
268         prefix = get_script_prefix()
269     return iri_to_uri(u'%s%s' % (prefix, get_resolver(urlconf).reverse(viewname,
270             *args, **kwargs)))
271
272 def clear_url_caches():
273     global _resolver_cache
274     global _callable_cache
275     _resolver_cache.clear()
276     _callable_cache.clear()
277
278 def set_script_prefix(prefix):
279     """
280     Sets the script prefix for the current thread.
281     """
282     if not prefix.endswith('/'):
283         prefix += '/'
284     _prefixes[currentThread()] = prefix
285
286 def get_script_prefix():
287     """
288     Returns the currently active script prefix. Useful for client code that
289     wishes to construct their own URLs manually (although accessing the request
290     instance is normally going to be a lot cleaner).
291     """
292     return _prefixes.get(currentThread(), u'/')
Note: See TracBrowser for help on using the browser.