Changeset 8760
- Timestamp:
- 08/31/08 06:11:20 (3 months ago)
- Files:
-
- django/trunk/django/core/urlresolvers.py (modified) (6 diffs)
- django/trunk/django/utils/regex_helper.py (added)
- django/trunk/docs/topics/http/urls.txt (modified) (1 diff)
- django/trunk/tests/regressiontests/templates/tests.py (modified) (1 diff)
- django/trunk/tests/regressiontests/urlpatterns_reverse/included_urls.py (added)
- django/trunk/tests/regressiontests/urlpatterns_reverse/tests.py (modified) (2 diffs)
- django/trunk/tests/regressiontests/urlpatterns_reverse/urls.py (added)
- django/trunk/tests/regressiontests/urlpatterns_reverse/views.py (added)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
django/trunk/django/core/urlresolvers.py
r8672 r8760 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 … … 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. … … 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, kwargs104 self.current_arg = 0105 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 group114 # 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 a119 # positional arg or not at all.120 try:121 value = self.args[self.current_arg]122 self.current_arg += 1123 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 += 1131 except IndexError:132 # The arg wasn't passed in.133 raise NoReverseMatch('Not enough positional arguments passed in')134 test_regex = grouped135 # Note we're using re.match here on purpose because the start of136 # 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): … … 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): … … 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) … … 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 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_match297 240 298 241 def resolve(path, urlconf=None): django/trunk/docs/topics/http/urls.txt
r8650 r8760 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 ----------- django/trunk/tests/regressiontests/templates/tests.py
r8746 r8760 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/'), django/trunk/tests/regressiontests/urlpatterns_reverse/tests.py
r7851 r8760 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, NoReverseMatch4 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) … … 37 76 self.assertEquals(got, expected) 38 77 39 if __name__ == "__main__":40 run_tests(1)
