Ticket #2977: new_reverse_urlresolver.2.patch

File new_reverse_urlresolver.2.patch, 11.1 KB (added by Chris Beaven, 10 years ago)
  • django/core/urlresolvers.py

     
    1111from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
    1212import re
    1313
     14# Set up these regular expressions outside the function so they only have to
     15# be compiled once.
     16re_bracket = re.compile(r'(?<!\\)((?:\\\\)*)([()])')  # Open or close bracket not preceeded by a single slash
     17re_has_named_group = re.compile(r'(?<!\\)(?:\\\\)*\(\?P')  #  '(?P' not preceeded by a single slash
     18re_type = type(re_bracket)
     19
     20re_unescape = re.compile(r'\\(.)|[$?*+^()]')
     21def unescape(value):
     22    """ Unescape a regex string """
     23    def repl(m):
     24        escaped = m.group(1)
     25        if escaped and re.match(r'[\ddDsSwW]', escaped):
     26            # These cases shouldn't ever come up - no match possible if they do.
     27            raise NoReverseMatch(r"Regular expression notation '\%s' was outside of a group so this regex is not reversable" % escaped)
     28        if escaped and escaped in 'AZbB':
     29            # These cases should just return nothing.
     30            return ''
     31        # For every other case: if it's the escaped version then return it without
     32        # a slash, otherwise return nothing.
     33        return escaped or ''
     34    return re_unescape.sub(repl, value)
     35
    1436class Resolver404(Http404):
    1537    pass
    1638
     
    4264
    4365    Raises NoReverseMatch if the args/kwargs aren't valid for the regex.
    4466    """
    45     # TODO: Handle nested parenthesis in the following regex.
    46     result = re.sub(r'\(([^)]+)\)', MatchChecker(args, kwargs), regex.pattern)
    47     return result.replace('^', '').replace('$', '')
     67    # Regex can either be a string or a regular epression.
     68    if isinstance(regex, re_type):
     69        regex = regex.pattern
     70    return ReverseRegexLookup(regex).check(args, kwargs)
    4871
    49 class MatchChecker(object):
    50     "Class used in reverse RegexURLPattern lookup."
    51     def __init__(self, args, kwargs):
    52         self.args, self.kwargs = args, kwargs
    53         self.current_arg = 0
     72def build_re(bits):
     73    output = []
     74    for bit in bits:
     75        if isinstance(bit, list):
     76            bit = build_re(bit)
     77        output.append(bit)
     78    return '(%s)' % ''.join(output)
    5479
    55     def __call__(self, match_obj):
    56         # match_obj.group(1) is the contents of the parenthesis.
    57         # First we need to figure out whether it's a named or unnamed group.
    58         #
    59         grouped = match_obj.group(1)
    60         m = re.search(r'^\?P<(\w+)>(.*?)$', grouped)
    61         if m: # If this was a named group...
    62             # m.group(1) is the name of the group
    63             # m.group(2) is the regex.
    64             try:
    65                 value = self.kwargs[m.group(1)]
    66             except KeyError:
    67                 # It was a named group, but the arg was passed in as a
    68                 # positional arg or not at all.
    69                 try:
    70                     value = self.args[self.current_arg]
    71                     self.current_arg += 1
    72                 except IndexError:
    73                     # The arg wasn't passed in.
    74                     raise NoReverseMatch('Not enough positional arguments passed in')
    75             test_regex = m.group(2)
    76         else: # Otherwise, this was a positional (unnamed) group.
    77             try:
    78                 value = self.args[self.current_arg]
    79                 self.current_arg += 1
    80             except IndexError:
    81                 # The arg wasn't passed in.
    82                 raise NoReverseMatch('Not enough positional arguments passed in')
    83             test_regex = grouped
    84         # Note we're using re.match here on purpose because the start of
    85         # to string needs to match.
    86         if not re.match(test_regex + '$', str(value)): # TODO: Unicode?
    87             raise NoReverseMatch("Value %r didn't match regular expression %r" % (value, test_regex))
    88         return str(value) # TODO: Unicode?
     80def tokenize(text):
     81    """
     82    Recursive tokenizer for regular expression parenthesis.
     83    """
     84    def parse(text, top=True, named_group=False):
     85        bits = []
     86        m = re_bracket.search(text)
     87        while m:
     88            before, text = text[:m.start()+len(m.group(1))], text[m.end():]
     89            if before:
     90                bits.append(before)
     91            if m.group(2) != '(':
     92                break
     93            inner_bits, text, named_group = parse(text, top=False, named_group=not top and named_group)
     94            if inner_bits:
     95                inline = named_group
     96                first_bit = inner_bits[0]
     97                if isinstance(first_bit, str):
     98                    if first_bit.startswith('?'):
     99                        # Regex extension notation.
     100                        if first_bit.startswith('?:'):
     101                            # No need to parse this non-grouping parenthesis.
     102                            inline = True
     103                            inner_bits[0] = first_bit[2:]
     104                        elif first_bit.startswith('?P'):
     105                            # Named group, set variable so higher levels will flatten.
     106                            named_group = True
     107                        else:
     108                            # Skip all other extension notation.
     109                            inner_bits = None
     110            if inner_bits:
     111                if inline:
     112                    bits.extend(inner_bits)
     113                else:
     114                    bits.append(inner_bits)
     115            m = re_bracket.search(text)
     116        return bits, text, named_group
     117    bits, text, named_group = parse(text)
     118    if text:
     119        bits.append(text)
     120    # Now tokenize the bits. Each token will either be a string or a regex.
     121    tokens = []
     122    count = 0
     123    for bit in bits:
     124        if isinstance(bit, list):
     125            # Build the regex here so it only has to be compiled once.
     126            bit = re.compile('%s$' % build_re(bit))
     127            count += 1
     128        tokens.append(bit)
     129    return tokens, count
    89130
     131class ReverseRegexLookup(object):
     132    def __init__(self, text):
     133        self.has_named_groups = bool(re_has_named_group.search(text))
     134        self.tokens, self.minimum_arguments = tokenize(text)
     135
     136    def check(self, args=[], kwargs={}):
     137        # Note: args and kwargs will be destroyed (using .pop()) so if you need
     138        # to keep using them, pass copies.
     139        if self.minimum_arguments > len(args) + len(kwargs):
     140            raise NoReverseMatch('Not enough arguments passed in')
     141        match = []
     142        args = list(args)
     143        kwargs = kwargs.copy()
     144        for token in self.tokens:
     145            if isinstance(token, re_type):   # A regex token.
     146                value = None
     147                # Is it a named argument?
     148                if token.groupindex:
     149                    try:
     150                        value = kwargs.pop(token.groupindex.keys()[0])
     151                    except KeyError:
     152                        # It was a named group, but the arg was passed in as a
     153                        # positional arg or not at all.
     154                        pass
     155                if value is None:
     156                    try:
     157                        value = args.pop(0)
     158                    except IndexError:
     159                        # The arg wasn't passed in.
     160                        raise NoReverseMatch('Not enough positional arguments passed in')
     161                value = str(value)   # TODO: Unicode?
     162                if not token.match(value):
     163                    raise NoReverseMatch("Value %r didn't match regular expression %r" % (value, token.pattern))
     164                match.append(value)
     165            else:    # A string token.
     166                match.append(token)
     167        match = ''.join(match)
     168        # Unescape special characters which could possibly be used in a URL and strip unused regular expression syntax.
     169        match = unescape(match)
     170        return match
     171
    90172class RegexURLPattern(object):
    91173    def __init__(self, regex, callback, default_args=None, name=None):
    92174        # regex is a string representing a regular expression.
     
    94176        # which represents the path to a module and a view function name, or a
    95177        # callable object (view).
    96178        self.regex = re.compile(regex)
     179        self.reverse_regex_lookup = ReverseRegexLookup(regex)
    97180        if callable(callback):
    98181            self._callback = callback
    99182        else:
     
    150233        return self.reverse_helper(*args, **kwargs)
    151234
    152235    def reverse_helper(self, *args, **kwargs):
    153         return reverse_helper(self.regex, *args, **kwargs)
     236        return self.reverse_regex_lookup.check(args, kwargs)
    154237
    155238class RegexURLResolver(object):
    156239    def __init__(self, regex, urlconf_name, default_kwargs=None):
    157240        # regex is a string representing a regular expression.
    158241        # urlconf_name is a string representing the module containing urlconfs.
    159242        self.regex = re.compile(regex)
     243        self.reverse_regex_lookup = ReverseRegexLookup(regex)
    160244        self.urlconf_name = urlconf_name
    161245        self.callback = None
    162246        self.default_kwargs = default_kwargs or {}
     
    230314        raise NoReverseMatch
    231315
    232316    def reverse_helper(self, lookup_view, *args, **kwargs):
     317        result = self.reverse_regex_lookup.check(args, kwargs)
     318        # .check() swallows used args, so the resolver is checking both itself
     319        # and its children using the one set of arguments.
    233320        sub_match = self.reverse(lookup_view, *args, **kwargs)
    234         result = reverse_helper(self.regex, *args, **kwargs)
    235321        return result + sub_match
    236322
    237323def resolve(path, urlconf=None):
  • tests/regressiontests/urlpatterns_reverse/tests.py

     
    2323    ('^people/(?P<state>\w\w)/(?P<name>\w+)/$', NoReverseMatch, [], {'name': 'adrian'}),
    2424    ('^people/(?P<state>\w\w)/(\w+)/$', NoReverseMatch, ['il'], {'name': 'adrian'}),
    2525    ('^people/(?P<state>\w\w)/(\w+)/$', 'people/il/adrian/', ['adrian'], {'state': 'il'}),
     26    ('^places?/$', 'places/', [], {}),
     27    ('^people/(?:name/)?', 'people/name/', [], {}),
     28    (r'^product/(?P<product>\w+)\+\(\$(?P<price>\d+(\.\d+)?)\)/$', 'product/chocolate+($2.00)/', ['2.00'], {'product': 'chocolate'}),
     29    (r'^places/(\d+|[a-z_]+)/', 'places/4/', [4], {}),
     30    (r'^places/(\d+|[a-z_]+)/', 'places/harlem/', ['harlem'], {}),
     31    ('^people/((?P<state>\w\w)/test)?/(\w+)/$', 'people/il/test/adrian/', ['adrian'], {'state': 'il'}),
     32    (r'^price/\$(\d+)/$', 'price/$10/', ['10'], {}),
    2633)
    2734
    2835class URLPatternReverse(unittest.TestCase):
    2936    def test_urlpattern_reverse(self):
    3037        for regex, expected, args, kwargs in test_data:
    3138            try:
    32                 got = reverse_helper(re.compile(regex), *args, **kwargs)
     39                got = reverse_helper(regex, *args, **kwargs)
    3340            except NoReverseMatch, e:
    3441                self.assertEqual(expected, NoReverseMatch)
    3542            else:
    3643                self.assertEquals(got, expected)
    3744
    3845if __name__ == "__main__":
    39     run_tests(1)
     46    unittest.main()
Back to Top