Ticket #5090: urlresolvers.py

File urlresolvers.py, 12.1 KB (added by Alex Nickolaenkov <nickolaenkov@…>, 8 years ago)

simplies patch

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