Django

Code

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

Revision 8672, 13.0 kB (checked in by jacob, 1 day ago)

Fixed #8221: added some better NoReverseMatch error strings. Thanks, mrts.

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