Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

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
  • Loading branch information...
commit a63a83e5d88cd1696d1c40e89f254f69116c6800 1 parent 84ef4a9
@malcolmt malcolmt authored
View
115 django/core/urlresolvers.py
@@ -13,12 +13,14 @@
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
+from django.utils.regex_helper import normalize
from django.utils.thread_support import currentThread
try:
reversed
except NameError:
from django.utils.itercompat import reversed # Python 2.3 fallback
+ from sets import Set as set
_resolver_cache = {} # Maps urlconf modules to RegexURLResolver instances.
_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[: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):
def __init__(self, regex, callback, default_args=None, name=None):
# regex is a string representing a regular expression.
@@ -194,21 +136,6 @@ def _get_callback(self):
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, 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):
def __init__(self, regex, urlconf_name, default_kwargs=None):
# regex is a string representing a regular expression.
@@ -225,12 +152,21 @@ def __repr__(self):
def _get_reverse_dict(self):
if not self._reverse_dict and hasattr(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):
- for key, value in pattern.reverse_dict.iteritems():
- self._reverse_dict[key] = (pattern,) + value
+ parent = normalize(pattern.regex.pattern)
+ 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:
- self._reverse_dict[pattern.callback] = (pattern,)
- self._reverse_dict[pattern.name] = (pattern,)
+ bits = normalize(p_pattern)
+ self._reverse_dict[pattern.callback] = bits, p_pattern
+ self._reverse_dict[pattern.name] = bits, p_pattern
return self._reverse_dict
reverse_dict = property(_get_reverse_dict)
@@ -281,20 +217,27 @@ def resolve500(self):
return self._resolve_special('500')
def reverse(self, lookup_view, *args, **kwargs):
+ if args and kwargs:
+ raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
try:
lookup_view = get_callable(lookup_view, True)
except (ImportError, AttributeError), e:
raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e))
- if lookup_view in self.reverse_dict:
- return u''.join([reverse_helper(part.regex, *args, **kwargs) for part in self.reverse_dict[lookup_view]])
+ possibilities, pattern = self.reverse_dict.get(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 "
"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):
return get_resolver(urlconf).resolve(path)
View
323 django/utils/regex_helper.py
@@ -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
7 docs/topics/http/urls.txt
@@ -612,6 +612,13 @@ arguments to use in the URL matching. For example::
.. _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()
-----------
View
3  tests/regressiontests/templates/tests.py
@@ -886,7 +886,8 @@ def get_template_tests(self):
### URL TAG ########################################################
# Successes
'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/'),
'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/'),
View
8 tests/regressiontests/urlpatterns_reverse/included_urls.py
@@ -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
93 tests/regressiontests/urlpatterns_reverse/tests.py
@@ -1,40 +1,77 @@
-"Unit tests for reverse URL lookup"
+"""
+Unit tests for reverse URL lookups.
+"""
-from django.core.urlresolvers import reverse_helper, NoReverseMatch
-import re, unittest
+from django.core.urlresolvers import reverse, NoReverseMatch
+from django.test import TestCase
test_data = (
- ('^places/(\d+)/$', 'places/3/', [3], {}),
- ('^places/(\d+)/$', 'places/3/', ['3'], {}),
- ('^places/(\d+)/$', NoReverseMatch, ['a'], {}),
- ('^places/(\d+)/$', NoReverseMatch, [], {}),
- ('^places/(?P<id>\d+)/$', 'places/3/', [], {'id': 3}),
- ('^people/(?P<name>\w+)/$', 'people/adrian/', ['adrian'], {}),
- ('^people/(?P<name>\w+)/$', 'people/adrian/', [], {'name': 'adrian'}),
- ('^people/(?P<name>\w+)/$', NoReverseMatch, ['name with spaces'], {}),
- ('^people/(?P<name>\w+)/$', NoReverseMatch, [], {'name': 'name with spaces'}),
- ('^people/(?P<name>\w+)/$', NoReverseMatch, [], {}),
- ('^hardcoded/$', 'hardcoded/', [], {}),
- ('^hardcoded/$', 'hardcoded/', ['any arg'], {}),
- ('^hardcoded/$', 'hardcoded/', [], {'kwarg': 'foo'}),
- ('^hardcoded/doc\\.pdf$', 'hardcoded/doc.pdf', [], {}),
- ('^people/(?P<state>\w\w)/(?P<name>\w+)/$', 'people/il/adrian/', [], {'state': 'il', 'name': 'adrian'}),
- ('^people/(?P<state>\w\w)/(?P<name>\d)/$', NoReverseMatch, [], {'state': 'il', 'name': 'adrian'}),
- ('^people/(?P<state>\w\w)/(?P<name>\w+)/$', NoReverseMatch, [], {'state': 'il'}),
- ('^people/(?P<state>\w\w)/(?P<name>\w+)/$', NoReverseMatch, [], {'name': 'adrian'}),
- ('^people/(?P<state>\w\w)/(\w+)/$', NoReverseMatch, ['il'], {'name': 'adrian'}),
- ('^people/(?P<state>\w\w)/(\w+)/$', 'people/il/adrian/', ['adrian'], {'state': 'il'}),
+ ('places', '/places/3/', [3], {}),
+ ('places', '/places/3/', ['3'], {}),
+ ('places', NoReverseMatch, ['a'], {}),
+ ('places', NoReverseMatch, [], {}),
+ ('places?', '/place/', [], {}),
+ ('places+', '/places/', [], {}),
+ ('places*', '/place/', [], {}),
+ ('places2?', '/', [], {}),
+ ('places2+', '/places/', [], {}),
+ ('places2*', '/', [], {}),
+ ('places3', '/places/4/', [4], {}),
+ ('places3', '/places/harlem/', ['harlem'], {}),
+ ('places3', NoReverseMatch, ['harlem64'], {}),
+ ('places4', '/places/3/', [], {'id': 3}),
+ ('people', NoReverseMatch, [], {}),
+ ('people', '/people/adrian/', ['adrian'], {}),
+ ('people', '/people/adrian/', [], {'name': 'adrian'}),
+ ('people', NoReverseMatch, ['name with spaces'], {}),
+ ('people', NoReverseMatch, [], {'name': 'name with spaces'}),
+ ('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):
- for regex, expected, args, kwargs in test_data:
+ for name, expected, args, kwargs in test_data:
try:
- got = reverse_helper(re.compile(regex), *args, **kwargs)
+ got = reverse(name, args=args, kwargs=kwargs)
except NoReverseMatch, e:
self.assertEqual(expected, NoReverseMatch)
else:
self.assertEquals(got, expected)
-if __name__ == "__main__":
- run_tests(1)
View
46 tests/regressiontests/urlpatterns_reverse/urls.py
@@ -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
2  tests/regressiontests/urlpatterns_reverse/views.py
@@ -0,0 +1,2 @@
+def empty_view(request, *args, **kwargs):
+ pass
Please sign in to comment.
Something went wrong with that request. Please try again.