Repository: tornadoweb/tornado
Pull request #1619 (2015-12-28 06:11:30)

reverse_url should unescape regex string
for #1609.

* implemented `tornado.util.re_unescape`
* use the `re_unescape` in `tornado.web.URLSpec._find_groups` so it would return unescaped urls
* add some test for the above


Patch:
diff --git a/tornado/util.py b/tornado/util.py
index 5e083961..a0401c14 100644
--- a/tornado/util.py
+++ b/tornado/util.py
@@ -14,6 +14,7 @@ from __future__ import absolute_import, division, print_function, with_statement
 
 import array
 import os
+import re
 import sys
 import zlib
 
@@ -172,6 +173,24 @@ def errno_from_exception(e):
         return None
 
 
+_alphanum = frozenset(
+    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
+
+def _re_unescape_replacement(match):
+    group = match.group(1)
+    if group[0] in _alphanum:
+        raise ValueError("cannot unescape '\\\\%s'" % group[0])
+    return group
+
+_re_unescape_pattern = re.compile(r'\\(.)', re.DOTALL)
+
+def re_unescape(s):
+    '''
+    unescape a string escaped by ``re.escape()``
+    '''
+    return _re_unescape_pattern.sub(_re_unescape_replacement, s)
+
+
 class Configurable(object):
     """Base class for configurable interfaces.
 
diff --git a/tornado/web.py b/tornado/web.py
index c51d5f68..b45363cc 100644
--- a/tornado/web.py
+++ b/tornado/web.py
@@ -90,7 +90,7 @@ from tornado import stack_context
 from tornado import template
 from tornado.escape import utf8, _unicode
 from tornado.util import (import_object, ObjectDict, raise_exc_info,
-                          unicode_type, _websocket_mask)
+                          unicode_type, _websocket_mask, re_unescape)
 from tornado.httputil import split_host_and_port
 
 
@@ -184,7 +184,7 @@ class RequestHandler(object):
 
     def initialize(self):
         """Hook for subclass initialization. Called for each request.
-          
+
         A dictionary passed as the third argument of a url spec will be
         supplied as keyword arguments to initialize().
 
@@ -3024,7 +3024,11 @@ class URLSpec(object):
                 if paren_loc >= 0:
                     pieces.append('%s' + fragment[paren_loc + 1:])
             else:
-                pieces.append(fragment)
+                try:
+                    unescaped_fragment = re_unescape(fragment)
+                except ValueError as exc:
+                    raise ValueError(exc.args[0] + '; invalid url: %r' % pattern)
+                pieces.append(re_unescape(fragment))
 
         return (''.join(pieces), self.regex.groups)
 


Test patch:
diff --git a/tornado/test/util_test.py b/tornado/test/util_test.py
index 98378fa0..70b6e7a1 100644
--- a/tornado/test/util_test.py
+++ b/tornado/test/util_test.py
@@ -1,11 +1,12 @@
 # coding: utf-8
 from __future__ import absolute_import, division, print_function, with_statement
+import re
 import sys
 import datetime
 
 import tornado.escape
 from tornado.escape import utf8
-from tornado.util import raise_exc_info, Configurable, exec_in, ArgReplacer, timedelta_to_seconds, import_object
+from tornado.util import raise_exc_info, Configurable, exec_in, ArgReplacer, timedelta_to_seconds, import_object, re_unescape
 from tornado.test.util import unittest
 
 try:
@@ -199,3 +200,23 @@ class ImportObjectTest(unittest.TestCase):
         # whether the thing being imported is a module or not.
         # This variant requires a byte string in python 2.
         self.assertIs(import_object(u'tornado.escape'), tornado.escape)
+
+
+class ReUnescapeTest(unittest.TestCase):
+    def test_re_unescape(self):
+        test_strings = (
+            '/favicon.ico',
+            'index.html',
+            'Hello, World!',
+            '!$@#%;',
+        )
+        for string in test_strings:
+            self.assertEqual(string, re_unescape(re.escape(string)))
+
+    def test_re_unescape_raises_error_on_invalid_input(self):
+        with self.assertRaises(ValueError):
+            re_unescape('\\d')
+        with self.assertRaises(ValueError):
+            re_unescape('\\b')
+        with self.assertRaises(ValueError):
+            re_unescape('\\Z')
diff --git a/tornado/test/web_test.py b/tornado/test/web_test.py
index 9f3582d1..7ae3d874 100644
--- a/tornado/test/web_test.py
+++ b/tornado/test/web_test.py
@@ -2763,3 +2763,8 @@ class ApplicationTest(AsyncTestCase):
         app = Application([])
         server = app.listen(0, address='127.0.0.1')
         server.stop()
+
+
+class URLSpecReverseTest(unittest.TestCase):
+    def test_reverse(self):
+        self.assertEqual('/favicon.ico', url(r'/favicon\.ico', None).reverse())