Repository: tornadoweb/tornado

Issue:
reverse_url() unescapes regex inconsistently
If you have a rule with an escaped regex character coming after a group in parentheses, like:
```python
URLSpec('/project/([0-9]+)/export/codebook\\.csv', ExportCodebookCsv)
```
it will correctly match `/project/1/export/codebook.csv`, but will reverse to `/project/1/export/codebook\.csv`.

Of course, if you remove the backslash:
```python
URLSpec('/project/([0-9]+)/export/codebook.csv', ExportCodebookCsv)
```
it will reverse to the wanted `/project/1/export/codebook.csv` but then it will also match `/project/1/export/codebookXcsv` (the dot matches any character).

This is because `re_unescape()` is only called for the first fragment, not the following ones; see no unescaping line 630:
https://github.com/tornadoweb/tornado/blob/ff985fe509513325462441e017f1ec31dda737c1/tornado/routing.py#L626-L638

See also #1619

Discussion:

Patch:
diff --git a/tornado/routing.py b/tornado/routing.py
index a35ac348..e137a728 100644
--- a/tornado/routing.py
+++ b/tornado/routing.py
@@ -627,7 +627,13 @@ class PathMatches(Matcher):
             if ")" in fragment:
                 paren_loc = fragment.index(")")
                 if paren_loc >= 0:
-                    pieces.append("%s" + fragment[paren_loc + 1 :])
+                    try:
+                        unescaped_fragment = re_unescape(fragment[paren_loc + 1 :])
+                    except ValueError:
+                        # If we can't unescape part of it, we can't
+                        # reverse this url.
+                        return (None, None)
+                    pieces.append("%s" + unescaped_fragment)
             else:
                 try:
                     unescaped_fragment = re_unescape(fragment)


Test patch:
diff --git a/tornado/test/web_test.py b/tornado/test/web_test.py
index 5908710a..0832f093 100644
--- a/tornado/test/web_test.py
+++ b/tornado/test/web_test.py
@@ -3107,6 +3107,10 @@ class URLSpecReverseTest(unittest.TestCase):
         self.assertEqual(
             "/api/v1/foo/bar", url(r"^/api/v1/foo/(\w+)$", None).reverse("bar")
         )
+        self.assertEqual(
+            "/api.v1/foo/5/icon.png",
+            url(r"/api\.v1/foo/([0-9]+)/icon\.png", None).reverse(5),
+        )
 
 
 class RedirectHandlerTest(WebTestCase):