~ubuntu-branches/ubuntu/quantal/python-django/quantal

« back to all changes in this revision

Viewing changes to django/core/urlresolvers.py

  • Committer: Bazaar Package Importer
  • Author(s): Scott James Remnant, Eddy Mulyono
  • Date: 2008-09-16 12:18:47 UTC
  • mfrom: (1.1.5 upstream) (4.1.1 lenny)
  • Revision ID: james.westby@ubuntu.com-20080916121847-mg225rg5mnsdqzr0
Tags: 1.0-1ubuntu1
* Merge from Debian (LP: #264191), remaining changes:
  - Run test suite on build.

[Eddy Mulyono]
* Update patch to workaround network test case failures.

Show diffs side-by-side

added added

removed removed

Lines of Context:
7
7
    (view_function, function_args, function_kwargs)
8
8
"""
9
9
 
 
10
import re
 
11
 
10
12
from django.http import Http404
11
13
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
12
 
import re
 
14
from django.utils.encoding import iri_to_uri, force_unicode, smart_str
 
15
from django.utils.functional import memoize
 
16
from django.utils.regex_helper import normalize
 
17
from django.utils.thread_support import currentThread
 
18
 
 
19
try:
 
20
    reversed
 
21
except NameError:
 
22
    from django.utils.itercompat import reversed     # Python 2.3 fallback
 
23
    from sets import Set as set
 
24
 
 
25
_resolver_cache = {} # Maps urlconf modules to RegexURLResolver instances.
 
26
_callable_cache = {} # Maps view and url pattern names to their view functions.
 
27
 
 
28
# SCRIPT_NAME prefixes for each thread are stored here. If there's no entry for
 
29
# the current thread (which is the only one we ever access), it is assumed to
 
30
# be empty.
 
31
_prefixes = {}
13
32
 
14
33
class Resolver404(Http404):
15
34
    pass
18
37
    # Don't make this raise an error when used in a template.
19
38
    silent_variable_failure = True
20
39
 
 
40
def get_callable(lookup_view, can_fail=False):
 
41
    """
 
42
    Convert a string version of a function name to the callable object.
 
43
 
 
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.
 
46
 
 
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__(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)
 
66
 
 
67
def get_resolver(urlconf):
 
68
    if urlconf is None:
 
69
        from django.conf import settings
 
70
        urlconf = settings.ROOT_URLCONF
 
71
    return RegexURLResolver(r'^/', urlconf)
 
72
get_resolver = memoize(get_resolver, _resolver_cache, 1)
 
73
 
21
74
def get_mod_func(callback):
22
75
    # Converts 'django.views.news.stories.story_detail' to
23
76
    # ['django.views.news.stories', 'story_detail']
27
80
        return callback, ''
28
81
    return callback[:dot], callback[dot+1:]
29
82
 
30
 
def reverse_helper(regex, *args, **kwargs):
31
 
    """
32
 
    Does a "reverse" lookup -- returns the URL for the given args/kwargs.
33
 
    The args/kwargs are applied to the given compiled regular expression.
34
 
    For example:
35
 
 
36
 
        >>> reverse_helper(re.compile('^places/(\d+)/$'), 3)
37
 
        'places/3/'
38
 
        >>> reverse_helper(re.compile('^places/(?P<id>\d+)/$'), id=3)
39
 
        'places/3/'
40
 
        >>> reverse_helper(re.compile('^people/(?P<state>\w\w)/(\w+)/$'), 'adrian', state='il')
41
 
        'people/il/adrian/'
42
 
 
43
 
    Raises NoReverseMatch if the args/kwargs aren't valid for the regex.
44
 
    """
45
 
    # TODO: Handle nested parenthesis in the following regex.
46
 
    result = re.sub(r'\(([^)]+)\)', MatchChecker(args, kwargs), regex.pattern)
47
 
    return result.replace('^', '').replace('$', '')
48
 
 
49
 
class MatchChecker(object):
50
 
    "Class used in reverse RegexURLPattern lookup."
51
 
    def __init__(self, args, kwargs):
52
 
        self.args, self.kwargs = args, kwargs
53
 
        self.current_arg = 0
54
 
 
55
 
    def __call__(self, match_obj):
56
 
        # match_obj.group(1) is the contents of the parenthesis.
57
 
        # First we need to figure out whether it's a named or unnamed group.
58
 
        #
59
 
        grouped = match_obj.group(1)
60
 
        m = re.search(r'^\?P<(\w+)>(.*?)$', grouped)
61
 
        if m: # If this was a named group...
62
 
            # m.group(1) is the name of the group
63
 
            # m.group(2) is the regex.
64
 
            try:
65
 
                value = self.kwargs[m.group(1)]
66
 
            except KeyError:
67
 
                # It was a named group, but the arg was passed in as a
68
 
                # positional arg or not at all.
69
 
                try:
70
 
                    value = self.args[self.current_arg]
71
 
                    self.current_arg += 1
72
 
                except IndexError:
73
 
                    # The arg wasn't passed in.
74
 
                    raise NoReverseMatch('Not enough positional arguments passed in')
75
 
            test_regex = m.group(2)
76
 
        else: # Otherwise, this was a positional (unnamed) group.
77
 
            try:
78
 
                value = self.args[self.current_arg]
79
 
                self.current_arg += 1
80
 
            except IndexError:
81
 
                # The arg wasn't passed in.
82
 
                raise NoReverseMatch('Not enough positional arguments passed in')
83
 
            test_regex = grouped
84
 
        # Note we're using re.match here on purpose because the start of
85
 
        # to string needs to match.
86
 
        if not re.match(test_regex + '$', str(value)): # TODO: Unicode?
87
 
            raise NoReverseMatch("Value %r didn't match regular expression %r" % (value, test_regex))
88
 
        return str(value) # TODO: Unicode?
89
 
 
90
83
class RegexURLPattern(object):
91
 
    def __init__(self, regex, callback, default_args=None):
 
84
    def __init__(self, regex, callback, default_args=None, name=None):
92
85
        # regex is a string representing a regular expression.
93
86
        # callback is either a string like 'foo.views.news.stories.story_detail'
94
87
        # which represents the path to a module and a view function name, or a
95
88
        # callable object (view).
96
 
        self.regex = re.compile(regex)
 
89
        self.regex = re.compile(regex, re.UNICODE)
97
90
        if callable(callback):
98
91
            self._callback = callback
99
92
        else:
100
93
            self._callback = None
101
94
            self._callback_str = callback
102
95
        self.default_args = default_args or {}
 
96
        self.name = name
 
97
 
 
98
    def __repr__(self):
 
99
        return '<%s %s %s>' % (self.__class__.__name__, self.name, self.regex.pattern)
 
100
 
 
101
    def add_prefix(self, prefix):
 
102
        """
 
103
        Adds the prefix string to a string-based callback.
 
104
        """
 
105
        if not prefix or not hasattr(self, '_callback_str'):
 
106
            return
 
107
        self._callback_str = prefix + '.' + self._callback_str
103
108
 
104
109
    def resolve(self, path):
105
110
        match = self.regex.search(path)
120
125
    def _get_callback(self):
121
126
        if self._callback is not None:
122
127
            return self._callback
123
 
        mod_name, func_name = get_mod_func(self._callback_str)
124
128
        try:
125
 
            self._callback = getattr(__import__(mod_name, {}, {}, ['']), func_name)
 
129
            self._callback = get_callable(self._callback_str)
126
130
        except ImportError, e:
 
131
            mod_name, _ = get_mod_func(self._callback_str)
127
132
            raise ViewDoesNotExist, "Could not import %s. Error was: %s" % (mod_name, str(e))
128
133
        except AttributeError, e:
 
134
            mod_name, func_name = get_mod_func(self._callback_str)
129
135
            raise ViewDoesNotExist, "Tried %s in module %s. Error was: %s" % (func_name, mod_name, str(e))
130
136
        return self._callback
131
137
    callback = property(_get_callback)
132
138
 
133
 
    def reverse(self, viewname, *args, **kwargs):
134
 
        mod_name, func_name = get_mod_func(viewname)
135
 
        try:
136
 
            lookup_view = getattr(__import__(mod_name, {}, {}, ['']), func_name)
137
 
        except (ImportError, AttributeError):
138
 
            raise NoReverseMatch
139
 
        if lookup_view != self.callback:
140
 
            raise NoReverseMatch
141
 
        return self.reverse_helper(*args, **kwargs)
142
 
 
143
 
    def reverse_helper(self, *args, **kwargs):
144
 
        return reverse_helper(self.regex, *args, **kwargs)
145
 
 
146
139
class RegexURLResolver(object):
147
140
    def __init__(self, regex, urlconf_name, default_kwargs=None):
148
141
        # regex is a string representing a regular expression.
149
142
        # urlconf_name is a string representing the module containing urlconfs.
150
 
        self.regex = re.compile(regex)
 
143
        self.regex = re.compile(regex, re.UNICODE)
151
144
        self.urlconf_name = urlconf_name
152
145
        self.callback = None
153
146
        self.default_kwargs = default_kwargs or {}
 
147
        self._reverse_dict = {}
 
148
 
 
149
    def __repr__(self):
 
150
        return '<%s %s %s>' % (self.__class__.__name__, self.urlconf_name, self.regex.pattern)
 
151
 
 
152
    def _get_reverse_dict(self):
 
153
        if not self._reverse_dict and hasattr(self.urlconf_module, 'urlpatterns'):
 
154
            for pattern in reversed(self.urlconf_module.urlpatterns):
 
155
                p_pattern = pattern.regex.pattern
 
156
                if p_pattern.startswith('^'):
 
157
                    p_pattern = p_pattern[1:]
 
158
                if isinstance(pattern, RegexURLResolver):
 
159
                    parent = normalize(pattern.regex.pattern)
 
160
                    for name, (matches, pat) in pattern.reverse_dict.iteritems():
 
161
                        new_matches = []
 
162
                        for piece, p_args in parent:
 
163
                            new_matches.extend([(piece + suffix, p_args + args)
 
164
                                    for (suffix, args) in matches])
 
165
                        self._reverse_dict[name] = new_matches, p_pattern + pat
 
166
                else:
 
167
                    bits = normalize(p_pattern)
 
168
                    self._reverse_dict[pattern.callback] = bits, p_pattern
 
169
                    self._reverse_dict[pattern.name] = bits, p_pattern
 
170
        return self._reverse_dict
 
171
    reverse_dict = property(_get_reverse_dict)
154
172
 
155
173
    def resolve(self, path):
156
174
        tried = []
164
182
                    tried.extend([(pattern.regex.pattern + '   ' + t) for t in e.args[0]['tried']])
165
183
                else:
166
184
                    if sub_match:
167
 
                        sub_match_dict = dict(self.default_kwargs, **sub_match[2])
168
 
                        return sub_match[0], sub_match[1], dict(match.groupdict(), **sub_match_dict)
 
185
                        sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()])
 
186
                        sub_match_dict.update(self.default_kwargs)
 
187
                        for k, v in sub_match[2].iteritems():
 
188
                            sub_match_dict[smart_str(k)] = v
 
189
                        return sub_match[0], sub_match[1], sub_match_dict
169
190
                    tried.append(pattern.regex.pattern)
170
191
            raise Resolver404, {'tried': tried, 'path': new_path}
171
192
 
173
194
        try:
174
195
            return self._urlconf_module
175
196
        except AttributeError:
176
 
            try:
177
 
                self._urlconf_module = __import__(self.urlconf_name, {}, {}, [''])
178
 
            except ValueError, e:
179
 
                # Invalid urlconf_name, such as "foo.bar." (note trailing period)
180
 
                raise ImproperlyConfigured, "Error while importing URLconf %r: %s" % (self.urlconf_name, e)
 
197
            self._urlconf_module = __import__(self.urlconf_name, {}, {}, [''])
181
198
            return self._urlconf_module
182
199
    urlconf_module = property(_get_urlconf_module)
183
200
 
200
217
        return self._resolve_special('500')
201
218
 
202
219
    def reverse(self, lookup_view, *args, **kwargs):
203
 
        if not callable(lookup_view):
204
 
            mod_name, func_name = get_mod_func(lookup_view)
205
 
            try:
206
 
                lookup_view = getattr(__import__(mod_name, {}, {}, ['']), func_name)
207
 
            except (ImportError, AttributeError):
208
 
                raise NoReverseMatch
209
 
        for pattern in self.urlconf_module.urlpatterns:
210
 
            if isinstance(pattern, RegexURLResolver):
211
 
                try:
212
 
                    return pattern.reverse_helper(lookup_view, *args, **kwargs)
213
 
                except NoReverseMatch:
214
 
                    continue
215
 
            elif pattern.callback == lookup_view:
216
 
                try:
217
 
                    return pattern.reverse_helper(*args, **kwargs)
218
 
                except NoReverseMatch:
219
 
                    continue
220
 
        raise NoReverseMatch
221
 
 
222
 
    def reverse_helper(self, lookup_view, *args, **kwargs):
223
 
        sub_match = self.reverse(lookup_view, *args, **kwargs)
224
 
        result = reverse_helper(self.regex, *args, **kwargs)
225
 
        return result + sub_match
 
220
        if args and kwargs:
 
221
            raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
 
222
        try:
 
223
            lookup_view = get_callable(lookup_view, True)
 
224
        except (ImportError, AttributeError), e:
 
225
            raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e))
 
226
        possibilities, pattern = self.reverse_dict.get(lookup_view, [(), ()])
 
227
        for result, params in possibilities:
 
228
            if args:
 
229
                if len(args) != len(params):
 
230
                    continue
 
231
                unicode_args = [force_unicode(val) for val in args]
 
232
                candidate =  result % dict(zip(params, unicode_args))
 
233
            else:
 
234
                if set(kwargs.keys()) != set(params):
 
235
                    continue
 
236
                unicode_kwargs = dict([(k, force_unicode(v)) for (k, v) in kwargs.items()])
 
237
                candidate = result % unicode_kwargs
 
238
            if re.search(u'^%s' % pattern, candidate, re.UNICODE):
 
239
                return candidate
 
240
        raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword "
 
241
                "arguments '%s' not found." % (lookup_view, args, kwargs))
226
242
 
227
243
def resolve(path, urlconf=None):
228
 
    if urlconf is None:
229
 
        from django.conf import settings
230
 
        urlconf = settings.ROOT_URLCONF
231
 
    resolver = RegexURLResolver(r'^/', urlconf)
232
 
    return resolver.resolve(path)
 
244
    return get_resolver(urlconf).resolve(path)
233
245
 
234
 
def reverse(viewname, urlconf=None, args=None, kwargs=None):
 
246
def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None):
235
247
    args = args or []
236
248
    kwargs = kwargs or {}
237
 
    if urlconf is None:
238
 
        from django.conf import settings
239
 
        urlconf = settings.ROOT_URLCONF
240
 
    resolver = RegexURLResolver(r'^/', urlconf)
241
 
    return '/' + resolver.reverse(viewname, *args, **kwargs)
 
249
    if prefix is None:
 
250
        prefix = get_script_prefix()
 
251
    return iri_to_uri(u'%s%s' % (prefix, get_resolver(urlconf).reverse(viewname,
 
252
            *args, **kwargs)))
 
253
 
 
254
def clear_url_caches():
 
255
    global _resolver_cache
 
256
    global _callable_cache
 
257
    _resolver_cache.clear()
 
258
    _callable_cache.clear()
 
259
 
 
260
def set_script_prefix(prefix):
 
261
    """
 
262
    Sets the script prefix for the current thread.
 
263
    """
 
264
    if not prefix.endswith('/'):
 
265
        prefix += '/'
 
266
    _prefixes[currentThread()] = prefix
 
267
 
 
268
def get_script_prefix():
 
269
    """
 
270
    Returns the currently active script prefix. Useful for client code that
 
271
    wishes to construct their own URLs manually (although accessing the request
 
272
    instance is normally going to be a lot cleaner).
 
273
    """
 
274
    return _prefixes.get(currentThread(), u'/')
 
275