django1/django/core/urlresolvers.py

191 lines
7.6 KiB
Python

"""
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
import re
class Resolver404(Http404):
pass
class NoReverseMatch(Exception):
pass
def get_mod_func(callback):
# Converts 'django.views.news.stories.story_detail' to
# ['django.views.news.stories', 'story_detail']
dot = callback.rindex('.')
return callback[:dot], callback[dot+1:]
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)
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 + '$', str(value)): # TODO: Unicode?
raise NoReverseMatch("Value %r didn't match regular expression %r" % (value, test_regex))
return str(value) # TODO: Unicode?
class RegexURLPattern:
def __init__(self, regex, callback, default_args=None):
# regex is a string representing a regular expression.
# callback is something like 'foo.views.news.stories.story_detail',
# which represents the path to a module and a view function name.
self.regex = re.compile(regex)
self.callback = callback
self.default_args = default_args or {}
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 = ()
if not kwargs:
args = match.groups()
# In both cases, pass any extra_kwargs as **kwargs.
kwargs.update(self.default_args)
try: # Lazily load self.func.
return self.func, args, kwargs
except AttributeError:
self.func = self.get_callback()
return self.func, args, kwargs
def get_callback(self):
mod_name, func_name = get_mod_func(self.callback)
try:
return getattr(__import__(mod_name, '', '', ['']), func_name)
except ImportError, e:
raise ViewDoesNotExist, "Could not import %s. Error was: %s" % (mod_name, str(e))
except AttributeError, e:
raise ViewDoesNotExist, "Tried %s in module %s. Error was: %s" % (func_name, mod_name, str(e))
def reverse(self, viewname, *args, **kwargs):
if viewname != self.callback:
raise NoReverseMatch
return self.reverse_helper(*args, **kwargs)
def reverse_helper(self, *args, **kwargs):
"""
Does a "reverse" lookup -- returns the URL for the given args/kwargs.
The args/kwargs are applied to the regular expression in this
RegexURLPattern. For example:
>>> RegexURLPattern('^places/(\d+)/$').reverse_helper(3)
'places/3/'
>>> RegexURLPattern('^places/(?P<id>\d+)/$').reverse_helper(id=3)
'places/3/'
>>> RegexURLPattern('^people/(?P<state>\w\w)/(\w+)/$').reverse_helper('adrian', state='il')
'people/il/adrian/'
Raises NoReverseMatch if the args/kwargs aren't valid for the RegexURLPattern.
"""
# TODO: Handle nested parenthesis in the following regex.
result = re.sub(r'\(([^)]+)\)', MatchChecker(args, kwargs), self.regex.pattern)
return result.replace('^', '').replace('$', '')
class RegexURLResolver(object):
def __init__(self, regex, urlconf_name):
# regex is a string representing a regular expression.
# urlconf_name is a string representing the module containing urlconfs.
self.regex = re.compile(regex)
self.urlconf_name = urlconf_name
self.callback = None
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:
return sub_match[0], sub_match[1], dict(match.groupdict(), **sub_match[2])
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 ValueError, e:
# Invalid urlconf_name, such as "foo.bar." (note trailing period)
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, viewname, *args, **kwargs):
for pattern in self.urlconf_module.urlpatterns:
if pattern.callback == viewname:
try:
return pattern.reverse_helper(*args, **kwargs)
except NoReverseMatch:
continue
raise NoReverseMatch