Repository: tornadoweb/tornado

Issue:
This unit test demonstrates a breaking change between 4.3 and 4.4
```
Traceback (most recent call last):
  File "/Users/aheinz-fe/Documents/GitHub/tornado/tornado/testing.py", line 380, in setUp
    self._app = self.get_app()
  File "/Users/aheinz-fe/Documents/GitHub/tornado/tornado/test/simple_httpclient_test.py", line 797, in get_app
    return Application([(r'^/api/v\d+/foo/(\w+)$', RawStringRegex)])
  File "/Users/aheinz-fe/Documents/GitHub/tornado/tornado/web.py", line 1814, in __init__
    self.add_handlers(".*$", handlers)
  File "/Users/aheinz-fe/Documents/GitHub/tornado/tornado/web.py", line 1875, in add_handlers
    spec = URLSpec(*spec)
  File "/Users/aheinz-fe/Documents/GitHub/tornado/tornado/web.py", line 3028, in __init__
    self._path, self._group_count = self._find_groups()
  File "/Users/aheinz-fe/Documents/GitHub/tornado/tornado/web.py", line 3062, in _find_groups
    raise ValueError(exc.args[0] + '; invalid url: %r' % pattern)
ValueError: cannot unescape '\\d'; invalid url: '/api/v\\d+/foo/(\\w+)'
```



Discussion:

Resolving patch:
diff --git a/tornado/web.py b/tornado/web.py
--- a/tornado/web.py
+++ b/tornado/web.py
@@ -3059,14 +3059,16 @@ def _find_groups(self):
                 try:
                     unescaped_fragment = re_unescape(fragment)
                 except ValueError as exc:
-                    raise ValueError(exc.args[0] + '; invalid url: %r' % pattern)
+                    # If we can't unescape part of it, we can't
+                    # reverse this url.
+                    return (None, None)
                 pieces.append(unescaped_fragment)
 
         return (''.join(pieces), self.regex.groups)
 
     def reverse(self, *args):
-        assert self._path is not None, \
-            "Cannot reverse url regex " + self.regex.pattern
+        if self._path is None:
+            raise ValueError("Cannot reverse url regex " + self.regex.pattern)
         assert len(args) == self._group_count, "required number of arguments "\
             "not found"
         if not len(args):


Test patch:
diff --git a/tornado/test/web_test.py b/tornado/test/web_test.py
--- a/tornado/test/web_test.py
+++ b/tornado/test/web_test.py
@@ -2811,3 +2811,26 @@ def test_listen(self):
 class URLSpecReverseTest(unittest.TestCase):
     def test_reverse(self):
         self.assertEqual('/favicon.ico', url(r'/favicon\.ico', None).reverse())
+        self.assertEqual('/favicon.ico', url(r'^/favicon\.ico$', None).reverse())
+
+    def test_non_reversible(self):
+        # URLSpecs are non-reversible if they include non-constant
+        # regex features outside capturing groups. Currently, this is
+        # only strictly enforced for backslash-escaped character
+        # classes.
+        paths = [
+            r'^/api/v\d+/foo/(\w+)$',
+        ]
+        for path in paths:
+            # A URLSpec can still be created even if it cannot be reversed.
+            url_spec = url(path, None)
+            try:
+                result = url_spec.reverse()
+                self.fail("did not get expected exception when reversing %s. "
+                          "result: %s" % (path, result))
+            except ValueError:
+                pass
+
+    def test_reverse_arguments(self):
+        self.assertEqual('/api/v1/foo/bar',
+                         url(r'^/api/v1/foo/(\w+)$', None).reverse('bar'))