diff -r 57b4279d8c4e -r 03e267d67478 app/django/core/urlresolvers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/django/core/urlresolvers.py Fri Jul 18 18:22:23 2008 +0000 @@ -0,0 +1,298 @@ +""" +This module converts requested URLs to callback view functions. + +RegexURLResolver is the main class here. Its resolve() method takes a URL (as +a string) and returns a tuple in this format: + + (view_function, function_args, function_kwargs) +""" + +from django.http import Http404 +from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist +from django.utils.encoding import iri_to_uri, force_unicode, smart_str +from django.utils.functional import memoize +import re + +try: + reversed +except NameError: + from django.utils.itercompat import reversed # Python 2.3 fallback + +_resolver_cache = {} # Maps urlconf modules to RegexURLResolver instances. +_callable_cache = {} # Maps view and url pattern names to their view functions. + +class Resolver404(Http404): + pass + +class NoReverseMatch(Exception): + # Don't make this raise an error when used in a template. + silent_variable_failure = True + +def get_callable(lookup_view, can_fail=False): + """ + Convert a string version of a function name to the callable object. + + If the lookup_view is not an import path, it is assumed to be a URL pattern + label and the original string is returned. + + If can_fail is True, lookup_view might be a URL pattern label, so errors + during the import fail and the string is returned. + """ + if not callable(lookup_view): + try: + # Bail early for non-ASCII strings (they can't be functions). + lookup_view = lookup_view.encode('ascii') + mod_name, func_name = get_mod_func(lookup_view) + if func_name != '': + lookup_view = getattr(__import__(mod_name, {}, {}, ['']), func_name) + except (ImportError, AttributeError): + if not can_fail: + raise + except UnicodeEncodeError: + pass + return lookup_view +get_callable = memoize(get_callable, _callable_cache, 1) + +def get_resolver(urlconf): + if urlconf is None: + from django.conf import settings + urlconf = settings.ROOT_URLCONF + return RegexURLResolver(r'^/', urlconf) +get_resolver = memoize(get_resolver, _resolver_cache, 1) + +def get_mod_func(callback): + # Converts 'django.views.news.stories.story_detail' to + # ['django.views.news.stories', 'story_detail'] + try: + dot = callback.rindex('.') + except ValueError: + return callback, '' + return callback[:dot], callback[dot+1:] + +def reverse_helper(regex, *args, **kwargs): + """ + Does a "reverse" lookup -- returns the URL for the given args/kwargs. + The args/kwargs are applied to the given compiled regular expression. + For example: + + >>> reverse_helper(re.compile('^places/(\d+)/$'), 3) + 'places/3/' + >>> reverse_helper(re.compile('^places/(?P\d+)/$'), id=3) + 'places/3/' + >>> reverse_helper(re.compile('^people/(?P\w\w)/(\w+)/$'), 'adrian', state='il') + 'people/il/adrian/' + + Raises NoReverseMatch if the args/kwargs aren't valid for the regex. + """ + # TODO: Handle nested parenthesis in the following regex. + result = re.sub(r'\(([^)]+)\)', MatchChecker(args, kwargs), regex.pattern) + return result.replace('^', '').replace('$', '') + +class MatchChecker(object): + "Class used in reverse RegexURLPattern lookup." + def __init__(self, args, kwargs): + self.args, self.kwargs = args, kwargs + self.current_arg = 0 + + def __call__(self, match_obj): + # match_obj.group(1) is the contents of the parenthesis. + # First we need to figure out whether it's a named or unnamed group. + # + grouped = match_obj.group(1) + m = re.search(r'^\?P<(\w+)>(.*?)$', grouped, re.UNICODE) + if m: # If this was a named group... + # m.group(1) is the name of the group + # m.group(2) is the regex. + try: + value = self.kwargs[m.group(1)] + except KeyError: + # It was a named group, but the arg was passed in as a + # positional arg or not at all. + try: + value = self.args[self.current_arg] + self.current_arg += 1 + except IndexError: + # The arg wasn't passed in. + raise NoReverseMatch('Not enough positional arguments passed in') + test_regex = m.group(2) + else: # Otherwise, this was a positional (unnamed) group. + try: + value = self.args[self.current_arg] + self.current_arg += 1 + except IndexError: + # The arg wasn't passed in. + raise NoReverseMatch('Not enough positional arguments passed in') + test_regex = grouped + # Note we're using re.match here on purpose because the start of + # to string needs to match. + if not re.match(test_regex + '$', force_unicode(value), re.UNICODE): + raise NoReverseMatch("Value %r didn't match regular expression %r" % (value, test_regex)) + return force_unicode(value) + +class RegexURLPattern(object): + def __init__(self, regex, callback, default_args=None, name=None): + # regex is a string representing a regular expression. + # callback is either a string like 'foo.views.news.stories.story_detail' + # which represents the path to a module and a view function name, or a + # callable object (view). + self.regex = re.compile(regex, re.UNICODE) + if callable(callback): + self._callback = callback + else: + self._callback = None + self._callback_str = callback + self.default_args = default_args or {} + self.name = name + + def __repr__(self): + return '<%s %s %s>' % (self.__class__.__name__, self.name, self.regex.pattern) + + def add_prefix(self, prefix): + """ + Adds the prefix string to a string-based callback. + """ + if not prefix or not hasattr(self, '_callback_str'): + return + self._callback_str = prefix + '.' + self._callback_str + + def resolve(self, path): + match = self.regex.search(path) + if match: + # If there are any named groups, use those as kwargs, ignoring + # non-named groups. Otherwise, pass all non-named arguments as + # positional arguments. + kwargs = match.groupdict() + if kwargs: + args = () + else: + args = match.groups() + # In both cases, pass any extra_kwargs as **kwargs. + kwargs.update(self.default_args) + + return self.callback, args, kwargs + + def _get_callback(self): + if self._callback is not None: + return self._callback + try: + self._callback = get_callable(self._callback_str) + except ImportError, e: + mod_name, _ = get_mod_func(self._callback_str) + raise ViewDoesNotExist, "Could not import %s. Error was: %s" % (mod_name, str(e)) + except AttributeError, e: + mod_name, func_name = get_mod_func(self._callback_str) + raise ViewDoesNotExist, "Tried %s in module %s. Error was: %s" % (func_name, mod_name, str(e)) + return self._callback + callback = property(_get_callback) + + def reverse(self, viewname, *args, **kwargs): + mod_name, func_name = get_mod_func(viewname) + try: + lookup_view = getattr(__import__(mod_name, {}, {}, ['']), func_name) + except (ImportError, AttributeError): + raise NoReverseMatch + if lookup_view != self.callback: + raise NoReverseMatch + return self.reverse_helper(*args, **kwargs) + + def reverse_helper(self, *args, **kwargs): + return reverse_helper(self.regex, *args, **kwargs) + +class RegexURLResolver(object): + def __init__(self, regex, urlconf_name, default_kwargs=None): + # regex is a string representing a regular expression. + # urlconf_name is a string representing the module containing urlconfs. + self.regex = re.compile(regex, re.UNICODE) + self.urlconf_name = urlconf_name + self.callback = None + self.default_kwargs = default_kwargs or {} + self._reverse_dict = {} + + def __repr__(self): + return '<%s %s %s>' % (self.__class__.__name__, self.urlconf_name, self.regex.pattern) + + def _get_reverse_dict(self): + if not self._reverse_dict and hasattr(self.urlconf_module, 'urlpatterns'): + for pattern in reversed(self.urlconf_module.urlpatterns): + if isinstance(pattern, RegexURLResolver): + for key, value in pattern.reverse_dict.iteritems(): + self._reverse_dict[key] = (pattern,) + value + else: + self._reverse_dict[pattern.callback] = (pattern,) + self._reverse_dict[pattern.name] = (pattern,) + return self._reverse_dict + reverse_dict = property(_get_reverse_dict) + + def resolve(self, path): + tried = [] + match = self.regex.search(path) + if match: + new_path = path[match.end():] + for pattern in self.urlconf_module.urlpatterns: + try: + sub_match = pattern.resolve(new_path) + except Resolver404, e: + tried.extend([(pattern.regex.pattern + ' ' + t) for t in e.args[0]['tried']]) + else: + if sub_match: + sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()]) + sub_match_dict.update(self.default_kwargs) + for k, v in sub_match[2].iteritems(): + sub_match_dict[smart_str(k)] = v + return sub_match[0], sub_match[1], sub_match_dict + tried.append(pattern.regex.pattern) + raise Resolver404, {'tried': tried, 'path': new_path} + + def _get_urlconf_module(self): + try: + return self._urlconf_module + except AttributeError: + try: + self._urlconf_module = __import__(self.urlconf_name, {}, {}, ['']) + except Exception, e: + # Either an invalid urlconf_name, such as "foo.bar.", or some + # kind of problem during the actual import. + raise ImproperlyConfigured, "Error while importing URLconf %r: %s" % (self.urlconf_name, e) + return self._urlconf_module + urlconf_module = property(_get_urlconf_module) + + def _get_url_patterns(self): + return self.urlconf_module.urlpatterns + url_patterns = property(_get_url_patterns) + + def _resolve_special(self, view_type): + callback = getattr(self.urlconf_module, 'handler%s' % view_type) + mod_name, func_name = get_mod_func(callback) + try: + return getattr(__import__(mod_name, {}, {}, ['']), func_name), {} + except (ImportError, AttributeError), e: + raise ViewDoesNotExist, "Tried %s. Error was: %s" % (callback, str(e)) + + def resolve404(self): + return self._resolve_special('404') + + def resolve500(self): + return self._resolve_special('500') + + def reverse(self, lookup_view, *args, **kwargs): + try: + lookup_view = get_callable(lookup_view, True) + except (ImportError, AttributeError): + raise NoReverseMatch + if lookup_view in self.reverse_dict: + return u''.join([reverse_helper(part.regex, *args, **kwargs) for part in self.reverse_dict[lookup_view]]) + raise NoReverseMatch + + def reverse_helper(self, lookup_view, *args, **kwargs): + sub_match = self.reverse(lookup_view, *args, **kwargs) + result = reverse_helper(self.regex, *args, **kwargs) + return result + sub_match + +def resolve(path, urlconf=None): + return get_resolver(urlconf).resolve(path) + +def reverse(viewname, urlconf=None, args=None, kwargs=None): + args = args or [] + kwargs = kwargs or {} + return iri_to_uri(u'/' + get_resolver(urlconf).reverse(viewname, *args, **kwargs)) +