A rewrite of the reverse URL parsing: the reverse() call and the "url" template tag.

This is fully backwards compatible, but it fixes a bunch of little bugs. Thanks
to SmileyChris and Ilya Semenov for some early patches in this area that were
incorporated into this change.

Fixed #2977, #4915, #6934, #7206.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@8760 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Malcolm Tredinnick 2008-08-31 11:11:20 +00:00
parent 84ef4a9b1d
commit a63a83e5d8
8 changed files with 482 additions and 115 deletions

View File

@ -13,12 +13,14 @@ from django.http import Http404
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
from django.utils.encoding import iri_to_uri, force_unicode, smart_str from django.utils.encoding import iri_to_uri, force_unicode, smart_str
from django.utils.functional import memoize from django.utils.functional import memoize
from django.utils.regex_helper import normalize
from django.utils.thread_support import currentThread from django.utils.thread_support import currentThread
try: try:
reversed reversed
except NameError: except NameError:
from django.utils.itercompat import reversed # Python 2.3 fallback from django.utils.itercompat import reversed # Python 2.3 fallback
from sets import Set as set
_resolver_cache = {} # Maps urlconf modules to RegexURLResolver instances. _resolver_cache = {} # Maps urlconf modules to RegexURLResolver instances.
_callable_cache = {} # Maps view and url pattern names to their view functions. _callable_cache = {} # Maps view and url pattern names to their view functions.
@ -78,66 +80,6 @@ def get_mod_func(callback):
return callback, '' return callback, ''
return callback[:dot], callback[dot+1:] 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<id>\d+)/$'), id=3)
'places/3/'
>>> reverse_helper(re.compile('^people/(?P<state>\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('$', '').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): class RegexURLPattern(object):
def __init__(self, regex, callback, default_args=None, name=None): def __init__(self, regex, callback, default_args=None, name=None):
# regex is a string representing a regular expression. # regex is a string representing a regular expression.
@ -194,21 +136,6 @@ class RegexURLPattern(object):
return self._callback return self._callback
callback = property(_get_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, e:
raise NoReverseMatch("Could not import '%s': %s" % (mod_name, e))
except AttributeError, e:
raise NoReverseMatch("'%s' has no attribute '%s'" % (mod_name, func_name))
if lookup_view != self.callback:
raise NoReverseMatch("Reversed view '%s' doesn't match the expected callback ('%s')." % (viewname, self.callback))
return self.reverse_helper(*args, **kwargs)
def reverse_helper(self, *args, **kwargs):
return reverse_helper(self.regex, *args, **kwargs)
class RegexURLResolver(object): class RegexURLResolver(object):
def __init__(self, regex, urlconf_name, default_kwargs=None): def __init__(self, regex, urlconf_name, default_kwargs=None):
# regex is a string representing a regular expression. # regex is a string representing a regular expression.
@ -225,12 +152,21 @@ class RegexURLResolver(object):
def _get_reverse_dict(self): def _get_reverse_dict(self):
if not self._reverse_dict and hasattr(self.urlconf_module, 'urlpatterns'): if not self._reverse_dict and hasattr(self.urlconf_module, 'urlpatterns'):
for pattern in reversed(self.urlconf_module.urlpatterns): for pattern in reversed(self.urlconf_module.urlpatterns):
p_pattern = pattern.regex.pattern
if p_pattern.startswith('^'):
p_pattern = p_pattern[1:]
if isinstance(pattern, RegexURLResolver): if isinstance(pattern, RegexURLResolver):
for key, value in pattern.reverse_dict.iteritems(): parent = normalize(pattern.regex.pattern)
self._reverse_dict[key] = (pattern,) + value for name, (matches, pat) in pattern.reverse_dict.iteritems():
new_matches = []
for piece, p_args in parent:
new_matches.extend([(piece + suffix, p_args + args)
for (suffix, args) in matches])
self._reverse_dict[name] = new_matches, p_pattern + pat
else: else:
self._reverse_dict[pattern.callback] = (pattern,) bits = normalize(p_pattern)
self._reverse_dict[pattern.name] = (pattern,) self._reverse_dict[pattern.callback] = bits, p_pattern
self._reverse_dict[pattern.name] = bits, p_pattern
return self._reverse_dict return self._reverse_dict
reverse_dict = property(_get_reverse_dict) reverse_dict = property(_get_reverse_dict)
@ -281,20 +217,27 @@ class RegexURLResolver(object):
return self._resolve_special('500') return self._resolve_special('500')
def reverse(self, lookup_view, *args, **kwargs): def reverse(self, lookup_view, *args, **kwargs):
if args and kwargs:
raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
try: try:
lookup_view = get_callable(lookup_view, True) lookup_view = get_callable(lookup_view, True)
except (ImportError, AttributeError), e: except (ImportError, AttributeError), e:
raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e)) raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e))
if lookup_view in self.reverse_dict: possibilities, pattern = self.reverse_dict.get(lookup_view, [(), ()])
return u''.join([reverse_helper(part.regex, *args, **kwargs) for part in self.reverse_dict[lookup_view]]) for result, params in possibilities:
if args:
if len(args) != len(params):
continue
candidate = result % dict(zip(params, args))
else:
if set(kwargs.keys()) != set(params):
continue
candidate = result % kwargs
if re.search('^%s' % pattern, candidate, re.UNICODE):
return candidate
raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword " raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword "
"arguments '%s' not found." % (lookup_view, args, kwargs)) "arguments '%s' not found." % (lookup_view, args, kwargs))
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): def resolve(path, urlconf=None):
return get_resolver(urlconf).resolve(path) return get_resolver(urlconf).resolve(path)

View File

@ -0,0 +1,323 @@
"""
Functions for reversing a regular expression (used in reverse URL resolving).
Used internally by Django and not intended for external use.
This is not, and is not intended to be, a complete reg-exp decompiler. It
should be good enough for a large class of URLS, however.
"""
# Mapping of an escape character to a representative of that class. So, e.g.,
# "\w" is replaced by "x" in a reverse URL. A value of None means to ignore
# this sequence. Any missing key is mapped to itself.
ESCAPE_MAPPINGS = {
"A": None,
"b": None,
"B": None,
"d": '0',
"D": "x",
"s": " ",
"S": "x",
"w": "x",
"W": "!",
"Z": None,
}
class Choice(list):
"""
Used to represent multiple possibilities at this point in a pattern string.
We use a distinguished type, rather than a list, so that the usage in the
code is clear.
"""
class Group(list):
"""
Used to represent a capturing group in the pattern string.
"""
class NonCapture(list):
"""
Used to represent a non-capturing group in the pattern string.
"""
def normalize(pattern):
"""
Given a reg-exp pattern, normalizes it to a list of forms that suffice for
reverse matching. This does the following:
(1) For any repeating sections, keeps the minimum number of occurrences
permitted (this means zero for optional groups).
(2) If an optional group includes parameters, include one occurrence of
that group (along with the zero occurrence case from step (1)).
(3) Select the first (essentially an arbitrary) element from any character
class. Select an arbitrary character for any unordered class (e.g. '.'
or '\w') in the pattern.
(5) Ignore comments and any of the reg-exp flags that won't change
what we construct ("iLmsu"). "(?x)" is an error, however.
(6) Raise an error on all other non-capturing (?...) forms (e.g.
look-ahead and look-behind matches) and any disjunctive ('|')
constructs.
Django's URLs for forward resolving are either all positional arguments or
all keyword arguments. That is assumed here, as well. Although reverse
resolving can be done using positional args when keyword args are
specified, the two cannot be mixed in the same reverse() call.
"""
# Do a linear scan to work out the special features of this pattern. The
# idea is that we scan once here and collect all the information we need to
# make future decisions.
result = []
non_capturing_groups = []
consume_next = True
pattern_iter = next_char(iter(pattern))
num_args = 0
# A "while" loop is used here because later on we need to be able to peek
# at the next character and possibly go around without consuming another
# one at the top of the loop.
ch, escaped = pattern_iter.next()
try:
while True:
if escaped:
result.append(ch)
elif ch == '.':
# Replace "any character" with an arbitrary representative.
result.append("x")
elif ch == '|':
# FIXME: One day we'll should do this, but not in 1.0.
raise NotImplementedError
elif ch == "^":
pass
elif ch == '$':
break
elif ch == ')':
# This can only be the end of a non-capturing group, since all
# other unescaped parentheses are handled by the grouping
# section later (and the full group is handled there).
#
# We regroup everything inside the capturing group so that it
# can be quantified, if necessary.
start = non_capturing_groups.pop()
inner = NonCapture(result[start:])
result = result[:start] + [inner]
elif ch == '[':
# Replace ranges with the first character in the range.
ch, escaped = pattern_iter.next()
result.append(ch)
ch, escaped = pattern_iter.next()
while escaped or ch != ']':
ch, escaped = pattern_iter.next()
elif ch == '(':
# Some kind of group.
ch, escaped = pattern_iter.next()
if ch != '?' or escaped:
# A positional group
name = "_%d" % num_args
num_args += 1
result.append(Group((("%%(%s)s" % name), name)))
walk_to_end(ch, pattern_iter)
else:
ch, escaped = pattern_iter.next()
if ch in "iLmsu#":
# All of these are ignorable. Walk to the end of the
# group.
walk_to_end(ch, pattern_iter)
elif ch == ':':
# Non-capturing group
non_capturing_groups.append(len(result))
elif ch != 'P':
# Anything else, other than a named group, is something
# we cannot reverse.
raise ValueError("Non-reversible reg-exp portion: '(?%s'" % ch)
else:
ch, escaped = pattern_iter.next()
if ch != '<':
raise ValueError("Non-reversible reg-exp portion: '(?P%s'" % ch)
# We are in a named capturing group. Extra the name and
# then skip to the end.
name = []
ch, escaped = pattern_iter.next()
while ch != '>':
name.append(ch)
ch, escaped = pattern_iter.next()
param = ''.join(name)
result.append(Group((("%%(%s)s" % param), param)))
walk_to_end(ch, pattern_iter)
elif ch in "*?+{":
# Quanitifers affect the previous item in the result list.
count, ch = get_quantifier(ch, pattern_iter)
if ch:
# We had to look ahead, but it wasn't need to compute the
# quanitifer, so use this character next time around the
# main loop.
consume_next = False
if count == 0:
if contains(result[-1], Group):
# If we are quantifying a capturing group (or
# something containing such a group) and the minimum is
# zero, we must also handle the case of one occurrence
# being present. All the quantifiers (except {0,0},
# which we conveniently ignore) that have a 0 minimum
# also allow a single occurrence.
result[-1] = Choice([None, result[-1]])
else:
result.pop()
elif count > 1:
result.extend([result[-1]] * (count - 1))
else:
# Anything else is a literal.
result.append(ch)
if consume_next:
ch, escaped = pattern_iter.next()
else:
consume_next = True
except StopIteration:
pass
except NotImplementedError:
# A case of using the disjunctive form. No results for you!
return zip([''], [[]])
return zip(*flatten_result(result))
def next_char(input_iter):
"""
An iterator that yields the next character from "pattern_iter", respecting
escape sequences. An escaped character is replaced by a representative of
its class (e.g. \w -> "x"). If the escaped character is one that is
skipped, it is not returned (the next character is returned instead).
Yields the next character, along with a boolean indicating whether it is a
raw (unescaped) character or not.
"""
for ch in input_iter:
if ch != '\\':
yield ch, False
continue
ch = input_iter.next()
representative = ESCAPE_MAPPINGS.get(ch, ch)
if representative is None:
continue
yield representative, True
def walk_to_end(ch, input_iter):
"""
The iterator is currently inside a capturing group. We want to walk to the
close of this group, skipping over any nested groups and handling escaped
parentheses correctly.
"""
if ch == '(':
nesting = 1
else:
nesting = 0
for ch, escaped in input_iter:
if escaped:
continue
elif ch == '(':
nesting += 1
elif ch == ')':
if not nesting:
return
nesting -= 1
def get_quantifier(ch, input_iter):
"""
Parse a quantifier from the input, where "ch" is the first character in the
quantifier.
Returns the minimum number of occurences permitted by the quantifier and
either None or the next character from the input_iter if the next character
is not part of the quantifier.
"""
if ch in '*?+':
try:
ch2, escaped = input_iter.next()
except StopIteration:
ch2 = None
if ch2 == '?':
ch2 = None
if ch == '+':
return 1, ch2
return 0, ch2
quant = []
while ch != '}':
ch, escaped = input_iter.next()
quant.append(ch)
values = ''.join(quant).split(',')
# Consume the trailing '?', if necessary.
try:
ch, escaped = input_iter.next()
except StopIteration:
ch = None
if ch == '?':
ch = None
return int(values[0]), ch
def contains(source, inst):
"""
Returns True if the "source" contains an instance of "inst". False,
otherwise.
"""
if isinstance(source, inst):
return True
if isinstance(source, NonCapture):
for elt in source:
if contains(elt, inst):
return True
return False
def flatten_result(source):
"""
Turns the given source sequence into a list of reg-exp possibilities and
their arguments. Returns a list of strings and a list of argument lists.
Each of the two lists will be of the same length.
"""
if source is None:
return [''], [[]]
if isinstance(source, Group):
if source[1] is None:
params = []
else:
params = [source[1]]
return [source[0]], [params]
result = ['']
result_args = [[]]
pos = last = 0
for pos, elt in enumerate(source):
if isinstance(elt, basestring):
continue
piece = ''.join(source[last:pos])
if isinstance(elt, Group):
piece += elt[0]
param = elt[1]
else:
param = None
last = pos + 1
for i in range(len(result)):
result[i] += piece
if param:
result_args[i].append(param)
if isinstance(elt, (Choice, NonCapture)):
if isinstance(elt, NonCapture):
elt = [elt]
inner_result, inner_args = [], []
for item in elt:
res, args = flatten_result(item)
inner_result.extend(res)
inner_args.extend(args)
new_result = []
new_args = []
for item, args in zip(result, result_args):
for i_item, i_args in zip(inner_result, inner_args):
new_result.append(item + i_item)
new_args.append(args[:] + i_args)
result = new_result
result_args = new_args
if pos >= last:
piece = ''.join(source[last:])
for i in range(len(result)):
result[i] += piece
return result, result_args

View File

@ -612,6 +612,13 @@ arguments to use in the URL matching. For example::
.. _URL pattern name: `Naming URL patterns`_ .. _URL pattern name: `Naming URL patterns`_
The ``reverse()`` function can reverse a large variety of regular expression
patterns for URLs, but not every possible one. The main restriction at the
moment is that the pattern cannot contain alternative choices using the
vertical bar (``"|"``) character. You can quite happily use such patterns for
matching against incoming URLs and sending them off to views, but you cannot
reverse such patterns.
permalink() permalink()
----------- -----------

View File

@ -886,7 +886,8 @@ class Templates(unittest.TestCase):
### URL TAG ######################################################## ### URL TAG ########################################################
# Successes # Successes
'url01': ('{% url regressiontests.templates.views.client client.id %}', {'client': {'id': 1}}, '/url_tag/client/1/'), 'url01': ('{% url regressiontests.templates.views.client client.id %}', {'client': {'id': 1}}, '/url_tag/client/1/'),
'url02': ('{% url regressiontests.templates.views.client_action client.id, action="update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'), 'url02': ('{% url regressiontests.templates.views.client_action id=client.id,action="update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'),
'url02a': ('{% url regressiontests.templates.views.client_action client.id,"update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'),
'url03': ('{% url regressiontests.templates.views.index %}', {}, '/url_tag/'), 'url03': ('{% url regressiontests.templates.views.index %}', {}, '/url_tag/'),
'url04': ('{% url named.client client.id %}', {'client': {'id': 1}}, '/url_tag/named-client/1/'), 'url04': ('{% url named.client client.id %}', {'client': {'id': 1}}, '/url_tag/named-client/1/'),
'url05': (u'{% url метка_оператора v %}', {'v': u'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'), 'url05': (u'{% url метка_оператора v %}', {'v': u'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'),

View File

@ -0,0 +1,8 @@
from django.conf.urls.defaults import *
from views import empty_view
urlpatterns = patterns('',
url(r'^$', empty_view, name="inner-nothing"),
url(r'^extra/(?P<extra>\w+)/$', empty_view, name="inner-extra"),
url(r'^(?P<one>\d+)|(?P<two>\d+)/$', empty_view, name="inner-disjunction"),
)

View File

@ -1,40 +1,77 @@
"Unit tests for reverse URL lookup" """
Unit tests for reverse URL lookups.
"""
from django.core.urlresolvers import reverse_helper, NoReverseMatch from django.core.urlresolvers import reverse, NoReverseMatch
import re, unittest from django.test import TestCase
test_data = ( test_data = (
('^places/(\d+)/$', 'places/3/', [3], {}), ('places', '/places/3/', [3], {}),
('^places/(\d+)/$', 'places/3/', ['3'], {}), ('places', '/places/3/', ['3'], {}),
('^places/(\d+)/$', NoReverseMatch, ['a'], {}), ('places', NoReverseMatch, ['a'], {}),
('^places/(\d+)/$', NoReverseMatch, [], {}), ('places', NoReverseMatch, [], {}),
('^places/(?P<id>\d+)/$', 'places/3/', [], {'id': 3}), ('places?', '/place/', [], {}),
('^people/(?P<name>\w+)/$', 'people/adrian/', ['adrian'], {}), ('places+', '/places/', [], {}),
('^people/(?P<name>\w+)/$', 'people/adrian/', [], {'name': 'adrian'}), ('places*', '/place/', [], {}),
('^people/(?P<name>\w+)/$', NoReverseMatch, ['name with spaces'], {}), ('places2?', '/', [], {}),
('^people/(?P<name>\w+)/$', NoReverseMatch, [], {'name': 'name with spaces'}), ('places2+', '/places/', [], {}),
('^people/(?P<name>\w+)/$', NoReverseMatch, [], {}), ('places2*', '/', [], {}),
('^hardcoded/$', 'hardcoded/', [], {}), ('places3', '/places/4/', [4], {}),
('^hardcoded/$', 'hardcoded/', ['any arg'], {}), ('places3', '/places/harlem/', ['harlem'], {}),
('^hardcoded/$', 'hardcoded/', [], {'kwarg': 'foo'}), ('places3', NoReverseMatch, ['harlem64'], {}),
('^hardcoded/doc\\.pdf$', 'hardcoded/doc.pdf', [], {}), ('places4', '/places/3/', [], {'id': 3}),
('^people/(?P<state>\w\w)/(?P<name>\w+)/$', 'people/il/adrian/', [], {'state': 'il', 'name': 'adrian'}), ('people', NoReverseMatch, [], {}),
('^people/(?P<state>\w\w)/(?P<name>\d)/$', NoReverseMatch, [], {'state': 'il', 'name': 'adrian'}), ('people', '/people/adrian/', ['adrian'], {}),
('^people/(?P<state>\w\w)/(?P<name>\w+)/$', NoReverseMatch, [], {'state': 'il'}), ('people', '/people/adrian/', [], {'name': 'adrian'}),
('^people/(?P<state>\w\w)/(?P<name>\w+)/$', NoReverseMatch, [], {'name': 'adrian'}), ('people', NoReverseMatch, ['name with spaces'], {}),
('^people/(?P<state>\w\w)/(\w+)/$', NoReverseMatch, ['il'], {'name': 'adrian'}), ('people', NoReverseMatch, [], {'name': 'name with spaces'}),
('^people/(?P<state>\w\w)/(\w+)/$', 'people/il/adrian/', ['adrian'], {'state': 'il'}), ('people2', '/people/name/', [], {}),
('people2a', '/people/name/fred/', ['fred'], {}),
('optional', '/optional/fred/', [], {'name': 'fred'}),
('optional', '/optional/fred/', ['fred'], {}),
('hardcoded', '/hardcoded/', [], {}),
('hardcoded2', '/hardcoded/doc.pdf', [], {}),
('people3', '/people/il/adrian/', [], {'state': 'il', 'name': 'adrian'}),
('people3', NoReverseMatch, [], {'state': 'il'}),
('people3', NoReverseMatch, [], {'name': 'adrian'}),
('people4', NoReverseMatch, [], {'state': 'il', 'name': 'adrian'}),
('people6', '/people/il/test/adrian/', ['il/test', 'adrian'], {}),
('people6', '/people//adrian/', ['adrian'], {}),
('range', '/character_set/a/', [], {}),
('range2', '/character_set/x/', [], {}),
('price', '/price/$10/', ['10'], {}),
('price2', '/price/$10/', ['10'], {}),
('price3', '/price/$10/', ['10'], {}),
('product', '/product/chocolate+($2.00)/', [], {'price': '2.00', 'product': 'chocolate'}),
('headlines', '/headlines/2007.5.21/', [], dict(year=2007, month=5, day=21)),
('windows', r'/windows_path/C:%5CDocuments%20and%20Settings%5Cspam/', [], dict(drive_name='C', path=r'Documents and Settings\spam')),
('special', r'/special_chars/+%5C$*/', [r'+\$*'], {}),
('special', NoReverseMatch, [''], {}),
('mixed', '/john/0/', [], {'name': 'john'}),
('repeats', '/repeats/a/', [], {}),
('repeats2', '/repeats/aa/', [], {}),
('insensitive', '/CaseInsensitive/fred', ['fred'], {}),
('test', '/test/1', [], {}),
('test2', '/test/2', [], {}),
('inner-nothing', '/outer/42/', [], {'outer': '42'}),
('inner-nothing', '/outer/42/', ['42'], {}),
('inner-nothing', NoReverseMatch, ['foo'], {}),
('inner-extra', '/outer/42/extra/inner/', [], {'extra': 'inner', 'outer': '42'}),
('inner-extra', '/outer/42/extra/inner/', ['42', 'inner'], {}),
('inner-extra', NoReverseMatch, ['fred', 'inner'], {}),
('disjunction', NoReverseMatch, ['foo'], {}),
('inner-disjunction', NoReverseMatch, ['10', '11'], {}),
) )
class URLPatternReverse(unittest.TestCase): class URLPatternReverse(TestCase):
urls = 'regressiontests.urlpatterns_reverse.urls'
def test_urlpattern_reverse(self): def test_urlpattern_reverse(self):
for regex, expected, args, kwargs in test_data: for name, expected, args, kwargs in test_data:
try: try:
got = reverse_helper(re.compile(regex), *args, **kwargs) got = reverse(name, args=args, kwargs=kwargs)
except NoReverseMatch, e: except NoReverseMatch, e:
self.assertEqual(expected, NoReverseMatch) self.assertEqual(expected, NoReverseMatch)
else: else:
self.assertEquals(got, expected) self.assertEquals(got, expected)
if __name__ == "__main__":
run_tests(1)

View File

@ -0,0 +1,46 @@
from django.conf.urls.defaults import *
from views import empty_view
urlpatterns = patterns('',
url(r'^places/(\d+)/$', empty_view, name='places'),
url(r'^places?/$', empty_view, name="places?"),
url(r'^places+/$', empty_view, name="places+"),
url(r'^places*/$', empty_view, name="places*"),
url(r'^(?:places/)?$', empty_view, name="places2?"),
url(r'^(?:places/)+$', empty_view, name="places2+"),
url(r'^(?:places/)*$', empty_view, name="places2*"),
url(r'^places/(\d+|[a-z_]+)/', empty_view, name="places3"),
url(r'^places/(?P<id>\d+)/$', empty_view, name="places4"),
url(r'^people/(?P<name>\w+)/$', empty_view, name="people"),
url(r'^people/(?:name/)', empty_view, name="people2"),
url(r'^people/(?:name/(\w+)/)?', empty_view, name="people2a"),
url(r'^optional/(?P<name>.*)/(?:.+/)?', empty_view, name="optional"),
url(r'^hardcoded/$', 'hardcoded/', empty_view, name="hardcoded"),
url(r'^hardcoded/doc\.pdf$', empty_view, name="hardcoded2"),
url(r'^people/(?P<state>\w\w)/(?P<name>\w+)/$', empty_view, name="people3"),
url(r'^people/(?P<state>\w\w)/(?P<name>\d)/$', empty_view, name="people4"),
url(r'^people/((?P<state>\w\w)/test)?/(\w+)/$', empty_view, name="people6"),
url(r'^character_set/[abcdef0-9]/$', empty_view, name="range"),
url(r'^character_set/[\w]/$', empty_view, name="range2"),
url(r'^price/\$(\d+)/$', empty_view, name="price"),
url(r'^price/[$](\d+)/$', empty_view, name="price2"),
url(r'^price/[\$](\d+)/$', empty_view, name="price3"),
url(r'^product/(?P<product>\w+)\+\(\$(?P<price>\d+(\.\d+)?)\)/$',
empty_view, name="product"),
url(r'^headlines/(?P<year>\d+)\.(?P<month>\d+)\.(?P<day>\d+)/$', empty_view,
name="headlines"),
url(r'^windows_path/(?P<drive_name>[A-Z]):\\(?P<path>.+)/$', empty_view,
name="windows"),
url(r'^special_chars/(.+)/$', empty_view, name="special"),
url(r'^(?P<name>.+)/\d+/$', empty_view, name="mixed"),
url(r'^repeats/a{1,2}/$', empty_view, name="repeats"),
url(r'^repeats/a{2,4}/$', empty_view, name="repeats2"),
url(r'^(?i)CaseInsensitive/(\w+)', empty_view, name="insensitive"),
url(r'^test/1/?', empty_view, name="test"),
url(r'^(?i)test/2/?$', empty_view, name="test2"),
url(r'^outer/(?P<outer>\d+)/',
include('regressiontests.urlpatterns_reverse.included_urls')),
# This is non-reversible, but we shouldn't blow up when parsing it.
url(r'^(?:foo|bar)(\w+)/$', empty_view, name="disjunction"),
)

View File

@ -0,0 +1,2 @@
def empty_view(request, *args, **kwargs):
pass