| 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." |
|---|