1 | Index: /home/tobryan1/workspace/django/django/core/urlresolvers.py |
---|
2 | =================================================================== |
---|
3 | --- /home/tobryan1/workspace/django/django/core/urlresolvers.py (revision 5478) |
---|
4 | +++ /home/tobryan1/workspace/django/django/core/urlresolvers.py (working copy) |
---|
5 | @@ -27,6 +27,18 @@ |
---|
6 | return callback, '' |
---|
7 | return callback[:dot], callback[dot+1:] |
---|
8 | |
---|
9 | +ESCAPE_CHARS = re.compile(r'([(){}|[\].^$*+?\\])') |
---|
10 | + |
---|
11 | +def escape(s): |
---|
12 | + """ |
---|
13 | + Escapes the characters that will get messed up later |
---|
14 | + """ |
---|
15 | + return ESCAPE_CHARS.sub(r'\\\1', s) |
---|
16 | + |
---|
17 | + |
---|
18 | +CHAR_CLASS_NEEDS_BACKSLASH = re.compile(r'\[([(){}|[\].^$*+?])\]') |
---|
19 | +CHAR_CLASS_NO_BACKSLASH = re.compile(r'\[(.)\]') |
---|
20 | + |
---|
21 | def reverse_helper(regex, *args, **kwargs): |
---|
22 | """ |
---|
23 | Does a "reverse" lookup -- returns the URL for the given args/kwargs. |
---|
24 | @@ -39,12 +51,68 @@ |
---|
25 | 'places/3/' |
---|
26 | >>> reverse_helper(re.compile('^people/(?P<state>\w\w)/(\w+)/$'), 'adrian', state='il') |
---|
27 | 'people/il/adrian/' |
---|
28 | - |
---|
29 | + >>> reverse_helper(re.compile(r'^prices/less_than_\$(?P<price>\d+)/$'), price='10') |
---|
30 | + 'prices/less_than_$10/' |
---|
31 | + >>> reverse_helper(re.compile(r'^prices/less_than_[$](?P<price>\d+)/$'), price='10') |
---|
32 | + 'prices/less_than_$10/' |
---|
33 | + >>> reverse_helper(re.compile(r'^headlines/(?P<year>\d+)\.(?P<month>\d+)\.(?P<day>\d+)/$'), year=2007, month=5, day=21) |
---|
34 | + 'headlines/2007.5.21/' |
---|
35 | + >>> reverse_helper(re.compile(r'^priests/(?P<name>\w+)\+/$'), name='maynard') |
---|
36 | + 'priests/maynard+/' |
---|
37 | + >>> reverse_helper(re.compile(r'^windows_path/(?P<drive_name>[A-Z]):\\\\(?P<path>.+)/$'), drive_name='C', path=r'Documents and Settings\\spam') |
---|
38 | + 'windows_path/C:\\\\Documents and Settings\\\\spam/' |
---|
39 | + >>> reverse_helper(re.compile(r'\\Aexpr\\\\b/expr2\\b\\\\Z/$')) |
---|
40 | + 'expr\\\\b/expr2\\\\Z/' |
---|
41 | + >>> reverse_helper(re.compile(r'^(?P<name>[^/]+)/\\d+/$'), name='john') |
---|
42 | + Traceback (most recent call last): |
---|
43 | + ... |
---|
44 | + NoReverseMatch: \d must be replaced by an argument in reverse lookup |
---|
45 | + |
---|
46 | Raises NoReverseMatch if the args/kwargs aren't valid for the regex. |
---|
47 | - """ |
---|
48 | + """ |
---|
49 | # TODO: Handle nested parenthesis in the following regex. |
---|
50 | - result = re.sub(r'\(([^)]+)\)', MatchChecker(args, kwargs), regex.pattern) |
---|
51 | - return result.replace('^', '').replace('$', '') |
---|
52 | + result = re.sub(r'\(([^)]+)\)', lambda m: escape(MatchChecker(args, kwargs)(m)), regex.pattern) |
---|
53 | + # TODO: octal characters make things even more complicated |
---|
54 | + # you can use a single character class to avoid escaping, e.g. [$] or [.]. |
---|
55 | + # normalize to backslash followed by character |
---|
56 | + result = CHAR_CLASS_NEEDS_BACKSLASH.sub(r'\\\1', result) |
---|
57 | + # you can put a single character in brackets (though why you would is |
---|
58 | + # beyond me); removes the brackets |
---|
59 | + result = CHAR_CLASS_NO_BACKSLASH.sub(r'\1', result) |
---|
60 | + # \A, \Z, \b, and \B match the empty string and should be removed, but |
---|
61 | + # only if preceded by an odd number of backslashes, otherwise the backslash |
---|
62 | + # right before is actually the second backslash in the backslash escape \\ |
---|
63 | + def delete_if_slashes_odd(m): |
---|
64 | + odd_slash_match = re.match(r'^(\\\\)*\\$', m.group('slashes')) |
---|
65 | + if odd_slash_match: |
---|
66 | + return odd_slash_match.group(1) |
---|
67 | + else: |
---|
68 | + return m.group(0) |
---|
69 | + result = re.sub(r'(?P<slashes>\\+)(?P<char>[AbBZ])', delete_if_slashes_odd, result) |
---|
70 | + # ^ and $ match the empty string and should be removed, but only if |
---|
71 | + # preceded by an even number of backslashes (including none), otherwise the |
---|
72 | + # backslash right before is escaping the literal \^ or \$ |
---|
73 | + def delete_if_slashes_even(m): |
---|
74 | + even_slash_match = re.match(r'^(\\\\)*$', m.group('slashes')) |
---|
75 | + if even_slash_match: |
---|
76 | + return m.group('slashes') |
---|
77 | + else: |
---|
78 | + return m.group(0) |
---|
79 | + result = re.sub(r'(?P<slashes>\\*)(?P<char>[$^])', delete_if_slashes_even, result) |
---|
80 | + # many characters are preceded by backslashes in regexes if the literal |
---|
81 | + # character is meant; as we go to a string, the backslash should go away. |
---|
82 | + # We should never find character classes that don't have a single |
---|
83 | + # replacement character. These are \number (the group matching expression), |
---|
84 | + # \d, \D, \s, \S, \w, and \W. If we find these at this point, we raise |
---|
85 | + # an exception. |
---|
86 | + def drop_backslash_if_valid(m): |
---|
87 | + char = m.group(1) |
---|
88 | + if re.match(r'[\ddDsSwW]', char): |
---|
89 | + raise NoReverseMatch(r'\%s must be replaced by an argument in reverse lookup' % char) |
---|
90 | + else: |
---|
91 | + return char |
---|
92 | + result = re.sub(r'\\([[\]{}()^$*+?.\\|\ddDsSwW])', drop_backslash_if_valid, result) |
---|
93 | + return result |
---|
94 | |
---|
95 | class MatchChecker(object): |
---|
96 | "Class used in reverse RegexURLPattern lookup." |
---|