Django

Code

Changeset 8760

Show
Ignore:
Timestamp:
08/31/08 06:11:20 (3 months ago)
Author:
mtredinnick
Message:

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.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/trunk/django/core/urlresolvers.py

    r8672 r8760  
    1414from django.utils.encoding import iri_to_uri, force_unicode, smart_str 
    1515from django.utils.functional import memoize 
     16from django.utils.regex_helper import normalize 
    1617from django.utils.thread_support import currentThread 
    1718 
     
    2021except NameError: 
    2122    from django.utils.itercompat import reversed     # Python 2.3 fallback 
     23    from sets import Set as set 
    2224 
    2325_resolver_cache = {} # Maps urlconf modules to RegexURLResolver instances. 
     
    7981    return callback[:dot], callback[dot+1:] 
    8082 
    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  
    14183class RegexURLPattern(object): 
    14284    def __init__(self, regex, callback, default_args=None, name=None): 
     
    195137    callback = property(_get_callback) 
    196138 
    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  
    212139class RegexURLResolver(object): 
    213140    def __init__(self, regex, urlconf_name, default_kwargs=None): 
     
    226153        if not self._reverse_dict and hasattr(self.urlconf_module, 'urlpatterns'): 
    227154            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:] 
    228158                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 
    231166                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 
    234170        return self._reverse_dict 
    235171    reverse_dict = property(_get_reverse_dict) 
     
    282218 
    283219    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()!") 
    284222        try: 
    285223            lookup_view = get_callable(lookup_view, True) 
    286224        except (ImportError, AttributeError), e: 
    287225            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 
    290238        raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword " 
    291239                "arguments '%s' not found." % (lookup_view, args, kwargs)) 
    292  
    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 
    297240 
    298241def resolve(path, urlconf=None): 
  • django/trunk/docs/topics/http/urls.txt

    r8650 r8760  
    613613.. _URL pattern name: `Naming URL patterns`_ 
    614614 
     615The ``reverse()`` function can reverse a large variety of regular expression 
     616patterns for URLs, but not every possible one. The main restriction at the 
     617moment is that the pattern cannot contain alternative choices using the 
     618vertical bar (``"|"``) character. You can quite happily use such patterns for 
     619matching against incoming URLs and sending them off to views, but you cannot 
     620reverse such patterns. 
     621 
    615622permalink() 
    616623----------- 
  • django/trunk/tests/regressiontests/templates/tests.py

    r8746 r8760  
    887887            # Successes 
    888888            '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/'), 
    890891            'url03': ('{% url regressiontests.templates.views.index %}', {}, '/url_tag/'), 
    891892            'url04': ('{% url named.client client.id %}', {'client': {'id': 1}}, '/url_tag/named-client/1/'), 
  • django/trunk/tests/regressiontests/urlpatterns_reverse/tests.py

    r7851 r8760  
    1 "Unit tests for reverse URL lookup" 
     1""" 
     2Unit tests for reverse URL lookups. 
     3""" 
    24 
    3 from django.core.urlresolvers import reverse_helper, NoReverseMatch 
    4 import re, unittest 
     5from django.core.urlresolvers import reverse, NoReverseMatch 
     6from django.test import TestCase 
    57 
    68test_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'], {}), 
    2764) 
    2865 
    29 class URLPatternReverse(unittest.TestCase): 
     66class URLPatternReverse(TestCase): 
     67    urls = 'regressiontests.urlpatterns_reverse.urls' 
     68 
    3069    def test_urlpattern_reverse(self): 
    31         for regex, expected, args, kwargs in test_data: 
     70        for name, expected, args, kwargs in test_data: 
    3271            try: 
    33                 got = reverse_helper(re.compile(regex), *args, **kwargs) 
     72                got = reverse(name, args=args, kwargs=kwargs) 
    3473            except NoReverseMatch, e: 
    3574                self.assertEqual(expected, NoReverseMatch) 
     
    3776                self.assertEquals(got, expected) 
    3877 
    39 if __name__ == "__main__": 
    40     run_tests(1)