Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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
Malcolm Tredinnick authored August 31, 2008
115  django/core/urlresolvers.py
@@ -13,12 +13,14 @@
13 13
 from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
14 14
 from django.utils.encoding import iri_to_uri, force_unicode, smart_str
15 15
 from django.utils.functional import memoize
  16
+from django.utils.regex_helper import normalize
16 17
 from django.utils.thread_support import currentThread
17 18
 
18 19
 try:
19 20
     reversed
20 21
 except NameError:
21 22
     from django.utils.itercompat import reversed     # Python 2.3 fallback
  23
+    from sets import Set as set
22 24
 
23 25
 _resolver_cache = {} # Maps urlconf modules to RegexURLResolver instances.
24 26
 _callable_cache = {} # Maps view and url pattern names to their view functions.
@@ -78,66 +80,6 @@ def get_mod_func(callback):
78 80
         return callback, ''
79 81
     return callback[:dot], callback[dot+1:]
80 82
 
81  
-def reverse_helper(regex, *args, **kwargs):
82  
-    """
83  
-    Does a "reverse" lookup -- returns the URL for the given args/kwargs.
84  
-    The args/kwargs are applied to the given compiled regular expression.
85  
-    For example:
86  
-
87  
-        >>> reverse_helper(re.compile('^places/(\d+)/$'), 3)
88  
-        'places/3/'
89  
-        >>> reverse_helper(re.compile('^places/(?P<id>\d+)/$'), id=3)
90  
-        'places/3/'
91  
-        >>> reverse_helper(re.compile('^people/(?P<state>\w\w)/(\w+)/$'), 'adrian', state='il')
92  
-        'people/il/adrian/'
93  
-
94  
-    Raises NoReverseMatch if the args/kwargs aren't valid for the regex.
95  
-    """
96  
-    # TODO: Handle nested parenthesis in the following regex.
97  
-    result = re.sub(r'\(([^)]+)\)', MatchChecker(args, kwargs), regex.pattern)
98  
-    return result.replace('^', '').replace('$', '').replace('\\', '')
99  
-
100  
-class MatchChecker(object):
101  
-    "Class used in reverse RegexURLPattern lookup."
102  
-    def __init__(self, args, kwargs):
103  
-        self.args, self.kwargs = args, kwargs
104  
-        self.current_arg = 0
105  
-
106  
-    def __call__(self, match_obj):
107  
-        # match_obj.group(1) is the contents of the parenthesis.
108  
-        # First we need to figure out whether it's a named or unnamed group.
109  
-        #
110  
-        grouped = match_obj.group(1)
111  
-        m = re.search(r'^\?P<(\w+)>(.*?)$', grouped, re.UNICODE)
112  
-        if m: # If this was a named group...
113  
-            # m.group(1) is the name of the group
114  
-            # m.group(2) is the regex.
115  
-            try:
116  
-                value = self.kwargs[m.group(1)]
117  
-            except KeyError:
118  
-                # It was a named group, but the arg was passed in as a
119  
-                # positional arg or not at all.
120  
-                try:
121  
-                    value = self.args[self.current_arg]
122  
-                    self.current_arg += 1
123  
-                except IndexError:
124  
-                    # The arg wasn't passed in.
125  
-                    raise NoReverseMatch('Not enough positional arguments passed in')
126  
-            test_regex = m.group(2)
127  
-        else: # Otherwise, this was a positional (unnamed) group.
128  
-            try:
129  
-                value = self.args[self.current_arg]
130  
-                self.current_arg += 1
131  
-            except IndexError:
132  
-                # The arg wasn't passed in.
133  
-                raise NoReverseMatch('Not enough positional arguments passed in')
134  
-            test_regex = grouped
135  
-        # Note we're using re.match here on purpose because the start of
136  
-        # to string needs to match.
137  
-        if not re.match(test_regex + '$', force_unicode(value), re.UNICODE):
138  
-            raise NoReverseMatch("Value %r didn't match regular expression %r" % (value, test_regex))
139  
-        return force_unicode(value)
140  
-
141 83
 class RegexURLPattern(object):
142 84
     def __init__(self, regex, callback, default_args=None, name=None):
143 85
         # regex is a string representing a regular expression.
@@ -194,21 +136,6 @@ def _get_callback(self):
194 136
         return self._callback
195 137
     callback = property(_get_callback)
196 138
 
197  
-    def reverse(self, viewname, *args, **kwargs):
198  
-        mod_name, func_name = get_mod_func(viewname)
199  
-        try:
200  
-            lookup_view = getattr(__import__(mod_name, {}, {}, ['']), func_name)
201  
-        except ImportError, e:
202  
-            raise NoReverseMatch("Could not import '%s': %s" % (mod_name, e))
203  
-        except AttributeError, e:
204  
-            raise NoReverseMatch("'%s' has no attribute '%s'" % (mod_name, func_name))
205  
-        if lookup_view != self.callback:
206  
-            raise NoReverseMatch("Reversed view '%s' doesn't match the expected callback ('%s')." % (viewname, self.callback))
207  
-        return self.reverse_helper(*args, **kwargs)
208  
-
209  
-    def reverse_helper(self, *args, **kwargs):
210  
-        return reverse_helper(self.regex, *args, **kwargs)
211  
-
212 139
 class RegexURLResolver(object):
213 140
     def __init__(self, regex, urlconf_name, default_kwargs=None):
214 141
         # regex is a string representing a regular expression.
@@ -225,12 +152,21 @@ def __repr__(self):
225 152
     def _get_reverse_dict(self):
226 153
         if not self._reverse_dict and hasattr(self.urlconf_module, 'urlpatterns'):
227 154
             for pattern in reversed(self.urlconf_module.urlpatterns):
  155
+                p_pattern = pattern.regex.pattern
  156
+                if p_pattern.startswith('^'):
  157
+                    p_pattern = p_pattern[1:]
228 158
                 if isinstance(pattern, RegexURLResolver):
229  
-                    for key, value in pattern.reverse_dict.iteritems():
230  
-                        self._reverse_dict[key] = (pattern,) + value
  159
+                    parent = normalize(pattern.regex.pattern)
  160
+                    for name, (matches, pat) in pattern.reverse_dict.iteritems():
  161
+                        new_matches = []
  162
+                        for piece, p_args in parent:
  163
+                            new_matches.extend([(piece + suffix, p_args + args)
  164
+                                    for (suffix, args) in matches])
  165
+                        self._reverse_dict[name] = new_matches, p_pattern + pat
231 166
                 else:
232  
-                    self._reverse_dict[pattern.callback] = (pattern,)
233  
-                    self._reverse_dict[pattern.name] = (pattern,)
  167
+                    bits = normalize(p_pattern)
  168
+                    self._reverse_dict[pattern.callback] = bits, p_pattern
  169
+                    self._reverse_dict[pattern.name] = bits, p_pattern
234 170
         return self._reverse_dict
235 171
     reverse_dict = property(_get_reverse_dict)
236 172
 
@@ -281,20 +217,27 @@ def resolve500(self):
281 217
         return self._resolve_special('500')
282 218
 
283 219
     def reverse(self, lookup_view, *args, **kwargs):
  220
+        if args and kwargs:
  221
+            raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
284 222
         try:
285 223
             lookup_view = get_callable(lookup_view, True)
286 224
         except (ImportError, AttributeError), e:
287 225
             raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e))
288  
-        if lookup_view in self.reverse_dict:
289  
-            return u''.join([reverse_helper(part.regex, *args, **kwargs) for part in self.reverse_dict[lookup_view]])
  226
+        possibilities, pattern = self.reverse_dict.get(lookup_view, [(), ()])
  227
+        for result, params in possibilities:
  228
+            if args:
  229
+                if len(args) != len(params):
  230
+                    continue
  231
+                candidate =  result % dict(zip(params, args))
  232
+            else:
  233
+                if set(kwargs.keys()) != set(params):
  234
+                    continue
  235
+                candidate = result % kwargs
  236
+            if re.search('^%s' % pattern, candidate, re.UNICODE):
  237
+                return candidate
290 238
         raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword "
291 239
                 "arguments '%s' not found." % (lookup_view, args, kwargs))
292 240
 
293  
-    def reverse_helper(self, lookup_view, *args, **kwargs):
294  
-        sub_match = self.reverse(lookup_view, *args, **kwargs)
295  
-        result = reverse_helper(self.regex, *args, **kwargs)
296  
-        return result + sub_match
297  
-
298 241
 def resolve(path, urlconf=None):
299 242
     return get_resolver(urlconf).resolve(path)
300 243
 
323  django/utils/regex_helper.py
... ...
@@ -0,0 +1,323 @@
  1
+"""
  2
+Functions for reversing a regular expression (used in reverse URL resolving).
  3
+Used internally by Django and not intended for external use.
  4
+
  5
+This is not, and is not intended to be, a complete reg-exp decompiler. It
  6
+should be good enough for a large class of URLS, however.
  7
+"""
  8
+
  9
+# Mapping of an escape character to a representative of that class. So, e.g.,
  10
+# "\w" is replaced by "x" in a reverse URL. A value of None means to ignore
  11
+# this sequence. Any missing key is mapped to itself.
  12
+ESCAPE_MAPPINGS = {
  13
+    "A": None,
  14
+    "b": None,
  15
+    "B": None,
  16
+    "d": '0',
  17
+    "D": "x",
  18
+    "s": " ",
  19
+    "S": "x",
  20
+    "w": "x",
  21
+    "W": "!",
  22
+    "Z": None,
  23
+}
  24
+
  25
+class Choice(list):
  26
+    """
  27
+    Used to represent multiple possibilities at this point in a pattern string.
  28
+    We use a distinguished type, rather than a list, so that the usage in the
  29
+    code is clear.
  30
+    """
  31
+
  32
+class Group(list):
  33
+    """
  34
+    Used to represent a capturing group in the pattern string.
  35
+    """
  36
+
  37
+class NonCapture(list):
  38
+    """
  39
+    Used to represent a non-capturing group in the pattern string.
  40
+    """
  41
+
  42
+def normalize(pattern):
  43
+    """
  44
+    Given a reg-exp pattern, normalizes it to a list of forms that suffice for
  45
+    reverse matching. This does the following:
  46
+
  47
+    (1) For any repeating sections, keeps the minimum number of occurrences
  48
+        permitted (this means zero for optional groups).
  49
+    (2) If an optional group includes parameters, include one occurrence of
  50
+        that group (along with the zero occurrence case from step (1)).
  51
+    (3) Select the first (essentially an arbitrary) element from any character
  52
+        class. Select an arbitrary character for any unordered class (e.g. '.'
  53
+        or '\w') in the pattern.
  54
+    (5) Ignore comments and any of the reg-exp flags that won't change
  55
+        what we construct ("iLmsu"). "(?x)" is an error, however.
  56
+    (6) Raise an error on all other non-capturing (?...) forms (e.g.
  57
+        look-ahead and look-behind matches) and any disjunctive ('|')
  58
+        constructs.
  59
+
  60
+    Django's URLs for forward resolving are either all positional arguments or
  61
+    all keyword arguments. That is assumed here, as well. Although reverse
  62
+    resolving can be done using positional args when keyword args are
  63
+    specified, the two cannot be mixed in the same reverse() call.
  64
+    """
  65
+    # Do a linear scan to work out the special features of this pattern. The
  66
+    # idea is that we scan once here and collect all the information we need to
  67
+    # make future decisions.
  68
+    result = []
  69
+    non_capturing_groups = []
  70
+    consume_next = True
  71
+    pattern_iter = next_char(iter(pattern))
  72
+    num_args = 0
  73
+
  74
+    # A "while" loop is used here because later on we need to be able to peek
  75
+    # at the next character and possibly go around without consuming another
  76
+    # one at the top of the loop.
  77
+    ch, escaped = pattern_iter.next()
  78
+    try:
  79
+        while True:
  80
+            if escaped:
  81
+                result.append(ch)
  82
+            elif ch == '.':
  83
+                # Replace "any character" with an arbitrary representative.
  84
+                result.append("x")
  85
+            elif ch == '|':
  86
+                # FIXME: One day we'll should do this, but not in 1.0.
  87
+                raise NotImplementedError
  88
+            elif ch == "^":
  89
+                pass
  90
+            elif ch == '$':
  91
+                break
  92
+            elif ch == ')':
  93
+                # This can only be the end of a non-capturing group, since all
  94
+                # other unescaped parentheses are handled by the grouping
  95
+                # section later (and the full group is handled there).
  96
+                #
  97
+                # We regroup everything inside the capturing group so that it
  98
+                # can be quantified, if necessary.
  99
+                start = non_capturing_groups.pop()
  100
+                inner = NonCapture(result[start:])
  101
+                result = result[:start] + [inner]
  102
+            elif ch == '[':
  103
+                # Replace ranges with the first character in the range.
  104
+                ch, escaped = pattern_iter.next()
  105
+                result.append(ch)
  106
+                ch, escaped = pattern_iter.next()
  107
+                while escaped or ch != ']':
  108
+                    ch, escaped = pattern_iter.next()
  109
+            elif ch == '(':
  110
+                # Some kind of group.
  111
+                ch, escaped = pattern_iter.next()
  112
+                if ch != '?' or escaped:
  113
+                    # A positional group
  114
+                    name = "_%d" % num_args
  115
+                    num_args += 1
  116
+                    result.append(Group((("%%(%s)s" % name), name)))
  117
+                    walk_to_end(ch, pattern_iter)
  118
+                else:
  119
+                    ch, escaped = pattern_iter.next()
  120
+                    if ch in "iLmsu#":
  121
+                        # All of these are ignorable. Walk to the end of the
  122
+                        # group.
  123
+                        walk_to_end(ch, pattern_iter)
  124
+                    elif ch == ':':
  125
+                        # Non-capturing group
  126
+                        non_capturing_groups.append(len(result))
  127
+                    elif ch != 'P':
  128
+                        # Anything else, other than a named group, is something
  129
+                        # we cannot reverse.
  130
+                        raise ValueError("Non-reversible reg-exp portion: '(?%s'" % ch)
  131
+                    else:
  132
+                        ch, escaped = pattern_iter.next()
  133
+                        if ch != '<':
  134
+                            raise ValueError("Non-reversible reg-exp portion: '(?P%s'" % ch)
  135
+                        # We are in a named capturing group. Extra the name and
  136
+                        # then skip to the end.
  137
+                        name = []
  138
+                        ch, escaped = pattern_iter.next()
  139
+                        while ch != '>':
  140
+                            name.append(ch)
  141
+                            ch, escaped = pattern_iter.next()
  142
+                        param = ''.join(name)
  143
+                        result.append(Group((("%%(%s)s" % param), param)))
  144
+                        walk_to_end(ch, pattern_iter)
  145
+            elif ch in "*?+{":
  146
+                # Quanitifers affect the previous item in the result list.
  147
+                count, ch = get_quantifier(ch, pattern_iter)
  148
+                if ch:
  149
+                    # We had to look ahead, but it wasn't need to compute the
  150
+                    # quanitifer, so use this character next time around the
  151
+                    # main loop.
  152
+                    consume_next = False
  153
+
  154
+                if count == 0:
  155
+                    if contains(result[-1], Group):
  156
+                        # If we are quantifying a capturing group (or
  157
+                        # something containing such a group) and the minimum is
  158
+                        # zero, we must also handle the case of one occurrence
  159
+                        # being present. All the quantifiers (except {0,0},
  160
+                        # which we conveniently ignore) that have a 0 minimum
  161
+                        # also allow a single occurrence.
  162
+                        result[-1] = Choice([None, result[-1]])
  163
+                    else:
  164
+                        result.pop()
  165
+                elif count > 1:
  166
+                    result.extend([result[-1]] * (count - 1))
  167
+            else:
  168
+                # Anything else is a literal.
  169
+                result.append(ch)
  170
+
  171
+            if consume_next:
  172
+                ch, escaped = pattern_iter.next()
  173
+            else:
  174
+                consume_next = True
  175
+    except StopIteration:
  176
+        pass
  177
+    except NotImplementedError:
  178
+        # A case of using the disjunctive form. No results for you!
  179
+        return zip([''],  [[]])
  180
+
  181
+    return zip(*flatten_result(result))
  182
+
  183
+def next_char(input_iter):
  184
+    """
  185
+    An iterator that yields the next character from "pattern_iter", respecting
  186
+    escape sequences. An escaped character is replaced by a representative of
  187
+    its class (e.g. \w -> "x"). If the escaped character is one that is
  188
+    skipped, it is not returned (the next character is returned instead).
  189
+
  190
+    Yields the next character, along with a boolean indicating whether it is a
  191
+    raw (unescaped) character or not.
  192
+    """
  193
+    for ch in input_iter:
  194
+        if ch != '\\':
  195
+            yield ch, False
  196
+            continue
  197
+        ch = input_iter.next()
  198
+        representative = ESCAPE_MAPPINGS.get(ch, ch)
  199
+        if representative is None:
  200
+            continue
  201
+        yield representative, True
  202
+
  203
+def walk_to_end(ch, input_iter):
  204
+    """
  205
+    The iterator is currently inside a capturing group. We want to walk to the
  206
+    close of this group, skipping over any nested groups and handling escaped
  207
+    parentheses correctly.
  208
+    """
  209
+    if ch == '(':
  210
+        nesting = 1
  211
+    else:
  212
+        nesting = 0
  213
+    for ch, escaped in input_iter:
  214
+        if escaped:
  215
+            continue
  216
+        elif ch == '(':
  217
+            nesting += 1
  218
+        elif ch == ')':
  219
+            if not nesting:
  220
+                return
  221
+            nesting -= 1
  222
+
  223
+def get_quantifier(ch, input_iter):
  224
+    """
  225
+    Parse a quantifier from the input, where "ch" is the first character in the
  226
+    quantifier.
  227
+
  228
+    Returns the minimum number of occurences permitted by the quantifier and
  229
+    either None or the next character from the input_iter if the next character
  230
+    is not part of the quantifier.
  231
+    """
  232
+    if ch in '*?+':
  233
+        try:
  234
+            ch2, escaped = input_iter.next()
  235
+        except StopIteration:
  236
+            ch2 = None
  237
+        if ch2 == '?':
  238
+            ch2 = None
  239
+        if ch == '+':
  240
+            return 1, ch2
  241
+        return 0, ch2
  242
+
  243
+    quant = []
  244
+    while ch != '}':
  245
+        ch, escaped = input_iter.next()
  246
+        quant.append(ch)
  247
+    values = ''.join(quant).split(',')
  248
+
  249
+    # Consume the trailing '?', if necessary.
  250
+    try:
  251
+        ch, escaped = input_iter.next()
  252
+    except StopIteration:
  253
+        ch = None
  254
+    if ch == '?':
  255
+        ch = None
  256
+    return int(values[0]), ch
  257
+
  258
+def contains(source, inst):
  259
+    """
  260
+    Returns True if the "source" contains an instance of "inst". False,
  261
+    otherwise.
  262
+    """
  263
+    if isinstance(source, inst):
  264
+        return True
  265
+    if isinstance(source, NonCapture):
  266
+        for elt in source:
  267
+            if contains(elt, inst):
  268
+                return True
  269
+    return False
  270
+
  271
+def flatten_result(source):
  272
+    """
  273
+    Turns the given source sequence into a list of reg-exp possibilities and
  274
+    their arguments. Returns a list of strings and a list of argument lists.
  275
+    Each of the two lists will be of the same length.
  276
+    """
  277
+    if source is None:
  278
+        return [''], [[]]
  279
+    if isinstance(source, Group):
  280
+        if source[1] is None:
  281
+            params = []
  282
+        else:
  283
+            params = [source[1]]
  284
+        return [source[0]], [params]
  285
+    result = ['']
  286
+    result_args = [[]]
  287
+    pos = last = 0
  288
+    for pos, elt in enumerate(source):
  289
+        if isinstance(elt, basestring):
  290
+            continue
  291
+        piece = ''.join(source[last:pos])
  292
+        if isinstance(elt, Group):
  293
+            piece += elt[0]
  294
+            param = elt[1]
  295
+        else:
  296
+            param = None
  297
+        last = pos + 1
  298
+        for i in range(len(result)):
  299
+            result[i] += piece
  300
+            if param:
  301
+                result_args[i].append(param)
  302
+        if isinstance(elt, (Choice, NonCapture)):
  303
+            if isinstance(elt, NonCapture):
  304
+                elt = [elt]
  305
+            inner_result, inner_args = [], []
  306
+            for item in elt:
  307
+                res, args = flatten_result(item)
  308
+                inner_result.extend(res)
  309
+                inner_args.extend(args)
  310
+            new_result = []
  311
+            new_args = []
  312
+            for item, args in zip(result, result_args):
  313
+                for i_item, i_args in zip(inner_result, inner_args):
  314
+                    new_result.append(item + i_item)
  315
+                    new_args.append(args[:] + i_args)
  316
+            result = new_result
  317
+            result_args = new_args
  318
+    if pos >= last:
  319
+        piece = ''.join(source[last:])
  320
+        for i in range(len(result)):
  321
+            result[i] += piece
  322
+    return result, result_args
  323
+
7  docs/topics/http/urls.txt
@@ -612,6 +612,13 @@ arguments to use in the URL matching. For example::
612 612
 
613 613
 .. _URL pattern name: `Naming URL patterns`_
614 614
 
  615
+The ``reverse()`` function can reverse a large variety of regular expression
  616
+patterns for URLs, but not every possible one. The main restriction at the
  617
+moment is that the pattern cannot contain alternative choices using the
  618
+vertical bar (``"|"``) character. You can quite happily use such patterns for
  619
+matching against incoming URLs and sending them off to views, but you cannot
  620
+reverse such patterns.
  621
+
615 622
 permalink()
616 623
 -----------
617 624
 
3  tests/regressiontests/templates/tests.py
@@ -886,7 +886,8 @@ def get_template_tests(self):
886 886
             ### URL TAG ########################################################
887 887
             # Successes
888 888
             'url01': ('{% url regressiontests.templates.views.client client.id %}', {'client': {'id': 1}}, '/url_tag/client/1/'),
889  
-            'url02': ('{% url regressiontests.templates.views.client_action client.id, action="update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'),
  889
+            'url02': ('{% url regressiontests.templates.views.client_action id=client.id,action="update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'),
  890
+            'url02a': ('{% url regressiontests.templates.views.client_action client.id,"update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'),
890 891
             'url03': ('{% url regressiontests.templates.views.index %}', {}, '/url_tag/'),
891 892
             'url04': ('{% url named.client client.id %}', {'client': {'id': 1}}, '/url_tag/named-client/1/'),
892 893
             'url05': (u'{% url метка_оператора v %}', {'v': u'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'),
8  tests/regressiontests/urlpatterns_reverse/included_urls.py
... ...
@@ -0,0 +1,8 @@
  1
+from django.conf.urls.defaults import *
  2
+from views import empty_view
  3
+
  4
+urlpatterns = patterns('',
  5
+    url(r'^$', empty_view, name="inner-nothing"),
  6
+    url(r'^extra/(?P<extra>\w+)/$', empty_view, name="inner-extra"),
  7
+    url(r'^(?P<one>\d+)|(?P<two>\d+)/$', empty_view, name="inner-disjunction"),
  8
+)
93  tests/regressiontests/urlpatterns_reverse/tests.py
... ...
@@ -1,40 +1,77 @@
1  
-"Unit tests for reverse URL lookup"
  1
+"""
  2
+Unit tests for reverse URL lookups.
  3
+"""
2 4
 
3  
-from django.core.urlresolvers import reverse_helper, NoReverseMatch
4  
-import re, unittest
  5
+from django.core.urlresolvers import reverse, NoReverseMatch
  6
+from django.test import TestCase
5 7
 
6 8
 test_data = (
7  
-    ('^places/(\d+)/$', 'places/3/', [3], {}),
8  
-    ('^places/(\d+)/$', 'places/3/', ['3'], {}),
9  
-    ('^places/(\d+)/$', NoReverseMatch, ['a'], {}),
10  
-    ('^places/(\d+)/$', NoReverseMatch, [], {}),
11  
-    ('^places/(?P<id>\d+)/$', 'places/3/', [], {'id': 3}),
12  
-    ('^people/(?P<name>\w+)/$', 'people/adrian/', ['adrian'], {}),
13  
-    ('^people/(?P<name>\w+)/$', 'people/adrian/', [], {'name': 'adrian'}),
14  
-    ('^people/(?P<name>\w+)/$', NoReverseMatch, ['name with spaces'], {}),
15  
-    ('^people/(?P<name>\w+)/$', NoReverseMatch, [], {'name': 'name with spaces'}),
16  
-    ('^people/(?P<name>\w+)/$', NoReverseMatch, [], {}),
17  
-    ('^hardcoded/$', 'hardcoded/', [], {}),
18  
-    ('^hardcoded/$', 'hardcoded/', ['any arg'], {}),
19  
-    ('^hardcoded/$', 'hardcoded/', [], {'kwarg': 'foo'}),
20  
-    ('^hardcoded/doc\\.pdf$', 'hardcoded/doc.pdf', [], {}),
21  
-    ('^people/(?P<state>\w\w)/(?P<name>\w+)/$', 'people/il/adrian/', [], {'state': 'il', 'name': 'adrian'}),
22  
-    ('^people/(?P<state>\w\w)/(?P<name>\d)/$', NoReverseMatch, [], {'state': 'il', 'name': 'adrian'}),
23  
-    ('^people/(?P<state>\w\w)/(?P<name>\w+)/$', NoReverseMatch, [], {'state': 'il'}),
24  
-    ('^people/(?P<state>\w\w)/(?P<name>\w+)/$', NoReverseMatch, [], {'name': 'adrian'}),
25  
-    ('^people/(?P<state>\w\w)/(\w+)/$', NoReverseMatch, ['il'], {'name': 'adrian'}),
26  
-    ('^people/(?P<state>\w\w)/(\w+)/$', 'people/il/adrian/', ['adrian'], {'state': 'il'}),
  9
+    ('places', '/places/3/', [3], {}),
  10
+    ('places', '/places/3/', ['3'], {}),
  11
+    ('places', NoReverseMatch, ['a'], {}),
  12
+    ('places', NoReverseMatch, [], {}),
  13
+    ('places?', '/place/', [], {}),
  14
+    ('places+', '/places/', [], {}),
  15
+    ('places*', '/place/', [], {}),
  16
+    ('places2?', '/', [], {}),
  17
+    ('places2+', '/places/', [], {}),
  18
+    ('places2*', '/', [], {}),
  19
+    ('places3', '/places/4/', [4], {}),
  20
+    ('places3', '/places/harlem/', ['harlem'], {}),
  21
+    ('places3', NoReverseMatch, ['harlem64'], {}),
  22
+    ('places4', '/places/3/', [], {'id': 3}),
  23
+    ('people', NoReverseMatch, [], {}),
  24
+    ('people', '/people/adrian/', ['adrian'], {}),
  25
+    ('people', '/people/adrian/', [], {'name': 'adrian'}),
  26
+    ('people', NoReverseMatch, ['name with spaces'], {}),
  27
+    ('people', NoReverseMatch, [], {'name': 'name with spaces'}),
  28
+    ('people2', '/people/name/', [], {}),
  29
+    ('people2a', '/people/name/fred/', ['fred'], {}),
  30
+    ('optional', '/optional/fred/', [], {'name': 'fred'}),
  31
+    ('optional', '/optional/fred/', ['fred'], {}),
  32
+    ('hardcoded', '/hardcoded/', [], {}),
  33
+    ('hardcoded2', '/hardcoded/doc.pdf', [], {}),
  34
+    ('people3', '/people/il/adrian/', [], {'state': 'il', 'name': 'adrian'}),
  35
+    ('people3', NoReverseMatch, [], {'state': 'il'}),
  36
+    ('people3', NoReverseMatch, [], {'name': 'adrian'}),
  37
+    ('people4', NoReverseMatch, [], {'state': 'il', 'name': 'adrian'}),
  38
+    ('people6', '/people/il/test/adrian/', ['il/test', 'adrian'], {}),
  39
+    ('people6', '/people//adrian/', ['adrian'], {}),
  40
+    ('range', '/character_set/a/', [], {}),
  41
+    ('range2', '/character_set/x/', [], {}),
  42
+    ('price', '/price/$10/', ['10'], {}),
  43
+    ('price2', '/price/$10/', ['10'], {}),
  44
+    ('price3', '/price/$10/', ['10'], {}),
  45
+    ('product', '/product/chocolate+($2.00)/', [], {'price': '2.00', 'product': 'chocolate'}),
  46
+    ('headlines', '/headlines/2007.5.21/', [], dict(year=2007, month=5, day=21)),
  47
+    ('windows', r'/windows_path/C:%5CDocuments%20and%20Settings%5Cspam/', [], dict(drive_name='C', path=r'Documents and Settings\spam')),
  48
+    ('special', r'/special_chars/+%5C$*/', [r'+\$*'], {}),
  49
+    ('special', NoReverseMatch, [''], {}),
  50
+    ('mixed', '/john/0/', [], {'name': 'john'}),
  51
+    ('repeats', '/repeats/a/', [], {}),
  52
+    ('repeats2', '/repeats/aa/', [], {}),
  53
+    ('insensitive', '/CaseInsensitive/fred', ['fred'], {}),
  54
+    ('test', '/test/1', [], {}),
  55
+    ('test2', '/test/2', [], {}),
  56
+    ('inner-nothing', '/outer/42/', [], {'outer': '42'}),
  57
+    ('inner-nothing', '/outer/42/', ['42'], {}),
  58
+    ('inner-nothing', NoReverseMatch, ['foo'], {}),
  59
+    ('inner-extra', '/outer/42/extra/inner/', [], {'extra': 'inner', 'outer': '42'}),
  60
+    ('inner-extra', '/outer/42/extra/inner/', ['42', 'inner'], {}),
  61
+    ('inner-extra', NoReverseMatch, ['fred', 'inner'], {}),
  62
+    ('disjunction', NoReverseMatch, ['foo'], {}),
  63
+    ('inner-disjunction', NoReverseMatch, ['10', '11'], {}),
27 64
 )
28 65
 
29  
-class URLPatternReverse(unittest.TestCase):
  66
+class URLPatternReverse(TestCase):
  67
+    urls = 'regressiontests.urlpatterns_reverse.urls'
  68
+
30 69
     def test_urlpattern_reverse(self):
31  
-        for regex, expected, args, kwargs in test_data:
  70
+        for name, expected, args, kwargs in test_data:
32 71
             try:
33  
-                got = reverse_helper(re.compile(regex), *args, **kwargs)
  72
+                got = reverse(name, args=args, kwargs=kwargs)
34 73
             except NoReverseMatch, e:
35 74
                 self.assertEqual(expected, NoReverseMatch)
36 75
             else:
37 76
                 self.assertEquals(got, expected)
38 77
 
39  
-if __name__ == "__main__":
40  
-    run_tests(1)
46  tests/regressiontests/urlpatterns_reverse/urls.py
... ...
@@ -0,0 +1,46 @@
  1
+from django.conf.urls.defaults import *
  2
+from views import empty_view
  3
+
  4
+urlpatterns = patterns('',
  5
+    url(r'^places/(\d+)/$', empty_view, name='places'),
  6
+    url(r'^places?/$', empty_view, name="places?"),
  7
+    url(r'^places+/$', empty_view, name="places+"),
  8
+    url(r'^places*/$', empty_view, name="places*"),
  9
+    url(r'^(?:places/)?$', empty_view, name="places2?"),
  10
+    url(r'^(?:places/)+$', empty_view, name="places2+"),
  11
+    url(r'^(?:places/)*$', empty_view, name="places2*"),
  12
+    url(r'^places/(\d+|[a-z_]+)/', empty_view, name="places3"),
  13
+    url(r'^places/(?P<id>\d+)/$', empty_view, name="places4"),
  14
+    url(r'^people/(?P<name>\w+)/$', empty_view, name="people"),
  15
+    url(r'^people/(?:name/)', empty_view, name="people2"),
  16
+    url(r'^people/(?:name/(\w+)/)?', empty_view, name="people2a"),
  17
+    url(r'^optional/(?P<name>.*)/(?:.+/)?', empty_view, name="optional"),
  18
+    url(r'^hardcoded/$', 'hardcoded/', empty_view, name="hardcoded"),
  19
+    url(r'^hardcoded/doc\.pdf$', empty_view, name="hardcoded2"),
  20
+    url(r'^people/(?P<state>\w\w)/(?P<name>\w+)/$', empty_view, name="people3"),
  21
+    url(r'^people/(?P<state>\w\w)/(?P<name>\d)/$', empty_view, name="people4"),
  22
+    url(r'^people/((?P<state>\w\w)/test)?/(\w+)/$', empty_view, name="people6"),
  23
+    url(r'^character_set/[abcdef0-9]/$', empty_view, name="range"),
  24
+    url(r'^character_set/[\w]/$', empty_view, name="range2"),
  25
+    url(r'^price/\$(\d+)/$', empty_view, name="price"),
  26
+    url(r'^price/[$](\d+)/$', empty_view, name="price2"),
  27
+    url(r'^price/[\$](\d+)/$', empty_view, name="price3"),
  28
+    url(r'^product/(?P<product>\w+)\+\(\$(?P<price>\d+(\.\d+)?)\)/$',
  29
+            empty_view, name="product"),
  30
+    url(r'^headlines/(?P<year>\d+)\.(?P<month>\d+)\.(?P<day>\d+)/$', empty_view,
  31
+            name="headlines"),
  32
+    url(r'^windows_path/(?P<drive_name>[A-Z]):\\(?P<path>.+)/$', empty_view,
  33
+            name="windows"),
  34
+    url(r'^special_chars/(.+)/$', empty_view, name="special"),
  35
+    url(r'^(?P<name>.+)/\d+/$', empty_view, name="mixed"),
  36
+    url(r'^repeats/a{1,2}/$', empty_view, name="repeats"),
  37
+    url(r'^repeats/a{2,4}/$', empty_view, name="repeats2"),
  38
+    url(r'^(?i)CaseInsensitive/(\w+)', empty_view, name="insensitive"),
  39
+    url(r'^test/1/?', empty_view, name="test"),
  40
+    url(r'^(?i)test/2/?$', empty_view, name="test2"),
  41
+    url(r'^outer/(?P<outer>\d+)/',
  42
+            include('regressiontests.urlpatterns_reverse.included_urls')),
  43
+
  44
+    # This is non-reversible, but we shouldn't blow up when parsing it.
  45
+    url(r'^(?:foo|bar)(\w+)/$', empty_view, name="disjunction"),
  46
+)
2  tests/regressiontests/urlpatterns_reverse/views.py
... ...
@@ -0,0 +1,2 @@
  1
+def empty_view(request, *args, **kwargs):
  2
+    pass

0 notes on commit a63a83e

Please sign in to comment.
Something went wrong with that request. Please try again.