Django

Code

Ticket #4594: reverse_helper.txt

File reverse_helper.txt, 4.8 kB (added by Todd O'Bryan <toddobryan@mac.com>, 1 year ago)
Line 
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,74 @@
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 +    #print result
54 +    # TODO: octal characters make things even more complicated
55 +    # you can use a single character class to avoid escaping, e.g. [$] or [.].
56 +    # normalize to backslash followed by character
57 +    result = CHAR_CLASS_NEEDS_BACKSLASH.sub(r'\\\1', result)
58 +    #print result
59 +    # you can put a single character in brackets (though why you would is
60 +    # beyond me); removes the brackets
61 +    result = CHAR_CLASS_NO_BACKSLASH.sub(r'\1', result)
62 +    #print result
63 +    # \A, \Z, \b, and \B match the empty string and should be removed, but
64 +    # only if preceded by an odd number of backslashes, otherwise the backslash
65 +    # right before is actually the second backslash in the backslash escape \\
66 +    def delete_if_slashes_odd(m):
67 +        odd_slash_match = re.match(r'^(\\\\)*\\$', m.group('slashes'))
68 +        if odd_slash_match:
69 +            return odd_slash_match.group(1)
70 +        else:
71 +            return m.group(0)
72 +    result = re.sub(r'(?P<slashes>\\+)(?P<char>[AbBZ])', delete_if_slashes_odd, result)
73 +    #print result
74 +    # ^ and $ match the empty string and should be removed, but only if
75 +    # preceded by an even number of backslashes (including none), otherwise the
76 +    # backslash right before is escaping the literal \^ or \$
77 +    def delete_if_slashes_even(m):
78 +        even_slash_match = re.match(r'^(\\\\)*$', m.group('slashes'))
79 +        if even_slash_match:
80 +            return m.group('slashes')
81 +        else:
82 +            return m.group(0)
83 +    result = re.sub(r'(?P<slashes>\\*)(?P<char>[$^])', delete_if_slashes_even, result)
84 +    #print result
85 +    # many characters are preceded by backslashes in regexes if the literal
86 +    # character is meant; as we go to a string, the backslash should go away.
87 +    # We should never find character classes that don't have a single
88 +    # replacement character. These are \number (the group matching expression),
89 +    # \d, \D, \s, \S, \w, and \W. If we find these at this point, we raise
90 +    # an exception.
91 +    def drop_backslash_if_valid(m):
92 +        char = m.group(1)
93 +        if re.match(r'[\ddDsSwW]', char):
94 +            raise NoReverseMatch(r'\%s must be replaced by an argument in reverse lookup' % char)
95 +        else:
96 +            return char
97 +    result = re.sub(r'\\([[\]{}()^$*+?.\\|\ddDsSwW])', drop_backslash_if_valid, result)
98 +    #print result
99 +    return result
100  
101  class MatchChecker(object):
102      "Class used in reverse RegexURLPattern lookup."