fix(routing): Restore compile_uri_template
Restore the compile_uri_template function to the routing module to ensure backwards compatibility with custom routing engines that rely on it. Fixes #532
This commit is contained in:
@@ -43,4 +43,4 @@ A custom routing engine may be specified when instantiating
|
||||
api = API(router=fancy)
|
||||
|
||||
.. automodule:: falcon.routing
|
||||
:members: create_http_method_map, CompiledRouter
|
||||
:members: create_http_method_map, compile_uri_template, CompiledRouter
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
from falcon.routing.compiled import CompiledRouter
|
||||
from falcon.routing.util import create_http_method_map # NOQA
|
||||
from falcon.routing.util import compile_uri_template # NOQA
|
||||
|
||||
|
||||
DefaultRouter = CompiledRouter
|
||||
|
||||
@@ -12,10 +12,72 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import re
|
||||
|
||||
import six
|
||||
|
||||
from falcon import HTTP_METHODS, responders
|
||||
from falcon.hooks import _wrap_with_hooks
|
||||
|
||||
|
||||
# NOTE(kgriffs): Published method; take care to avoid breaking changes.
|
||||
def compile_uri_template(template):
|
||||
"""Compile the given URI template string into a pattern matcher.
|
||||
|
||||
This function can be used to construct custom routing engines that
|
||||
iterate through a list of possible routes, attempting to match
|
||||
an incoming request against each route's compiled regular expression.
|
||||
|
||||
Each field is converted to a named group, so that when a match
|
||||
is found, the fields can be easily extracted using
|
||||
:py:meth:`re.MatchObject.groupdict`.
|
||||
|
||||
This function does not support the more flexible templating
|
||||
syntax used in the default router. Only simple paths with bracketed
|
||||
field expressions are recognized. For example::
|
||||
|
||||
/
|
||||
/books
|
||||
/books/{isbn}
|
||||
/books/{isbn}/characters
|
||||
/books/{isbn}/characters/{name}
|
||||
|
||||
Also, note that if the template contains a trailing slash character,
|
||||
it will be stripped in order to normalize the routing logic.
|
||||
|
||||
Args:
|
||||
template(str): The template to compile. Note that field names are
|
||||
restricted to ASCII a-z, A-Z, and the underscore character.
|
||||
|
||||
Returns:
|
||||
tuple: (template_field_names, template_regex)
|
||||
"""
|
||||
|
||||
if not isinstance(template, six.string_types):
|
||||
raise TypeError('uri_template is not a string')
|
||||
|
||||
if not template.startswith('/'):
|
||||
raise ValueError("uri_template must start with '/'")
|
||||
|
||||
if '//' in template:
|
||||
raise ValueError("uri_template may not contain '//'")
|
||||
|
||||
if template != '/' and template.endswith('/'):
|
||||
template = template[:-1]
|
||||
|
||||
expression_pattern = r'{([a-zA-Z][a-zA-Z_]*)}'
|
||||
|
||||
# Get a list of field names
|
||||
fields = set(re.findall(expression_pattern, template))
|
||||
|
||||
# Convert Level 1 var patterns to equivalent named regex groups
|
||||
escaped = re.sub(r'[\.\(\)\[\]\?\*\+\^\|]', r'\\\g<0>', template)
|
||||
pattern = re.sub(expression_pattern, r'(?P<\1>[^/]+)', escaped)
|
||||
pattern = r'\A' + pattern + r'\Z'
|
||||
|
||||
return fields, re.compile(pattern, re.IGNORECASE)
|
||||
|
||||
|
||||
def create_http_method_map(resource, before, after):
|
||||
"""Maps HTTP methods (e.g., 'GET', 'POST') to methods of a resource object.
|
||||
|
||||
|
||||
96
tests/test_uri_templates_legacy.py
Normal file
96
tests/test_uri_templates_legacy.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import ddt
|
||||
|
||||
import falcon
|
||||
from falcon import routing
|
||||
import falcon.testing as testing
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestUriTemplates(testing.TestBase):
|
||||
|
||||
def test_string_type_required(self):
|
||||
self.assertRaises(TypeError, routing.compile_uri_template, 42)
|
||||
self.assertRaises(TypeError, routing.compile_uri_template, falcon.API)
|
||||
|
||||
def test_template_must_start_with_slash(self):
|
||||
self.assertRaises(ValueError, routing.compile_uri_template, 'this')
|
||||
self.assertRaises(ValueError, routing.compile_uri_template, 'this/that')
|
||||
|
||||
def test_template_may_not_contain_double_slash(self):
|
||||
self.assertRaises(ValueError, routing.compile_uri_template, '//')
|
||||
self.assertRaises(ValueError, routing.compile_uri_template, 'a//')
|
||||
self.assertRaises(ValueError, routing.compile_uri_template, '//b')
|
||||
self.assertRaises(ValueError, routing.compile_uri_template, 'a//b')
|
||||
self.assertRaises(ValueError, routing.compile_uri_template, 'a/b//')
|
||||
self.assertRaises(ValueError, routing.compile_uri_template, 'a/b//c')
|
||||
|
||||
def test_root(self):
|
||||
fields, pattern = routing.compile_uri_template('/')
|
||||
self.assertFalse(fields)
|
||||
self.assertFalse(pattern.match('/x'))
|
||||
|
||||
result = pattern.match('/')
|
||||
self.assertTrue(result)
|
||||
self.assertFalse(result.groupdict())
|
||||
|
||||
@ddt.data('/hello', '/hello/world', '/hi/there/how/are/you')
|
||||
def test_no_fields(self, path):
|
||||
fields, pattern = routing.compile_uri_template(path)
|
||||
self.assertFalse(fields)
|
||||
self.assertFalse(pattern.match(path[:-1]))
|
||||
|
||||
result = pattern.match(path)
|
||||
self.assertTrue(result)
|
||||
self.assertFalse(result.groupdict())
|
||||
|
||||
def test_one_field(self):
|
||||
fields, pattern = routing.compile_uri_template('/{name}')
|
||||
self.assertEqual(fields, set(['name']))
|
||||
|
||||
result = pattern.match('/Kelsier')
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(result.groupdict(), {'name': 'Kelsier'})
|
||||
|
||||
fields, pattern = routing.compile_uri_template('/character/{name}')
|
||||
self.assertEqual(fields, set(['name']))
|
||||
|
||||
result = pattern.match('/character/Kelsier')
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(result.groupdict(), {'name': 'Kelsier'})
|
||||
|
||||
fields, pattern = routing.compile_uri_template('/character/{name}/profile')
|
||||
self.assertEqual(fields, set(['name']))
|
||||
|
||||
self.assertFalse(pattern.match('/character'))
|
||||
self.assertFalse(pattern.match('/character/Kelsier'))
|
||||
self.assertFalse(pattern.match('/character/Kelsier/'))
|
||||
|
||||
result = pattern.match('/character/Kelsier/profile')
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(result.groupdict(), {'name': 'Kelsier'})
|
||||
|
||||
@ddt.data('', '/')
|
||||
def test_two_fields(self, postfix):
|
||||
path = '/book/{id}/characters/{name}' + postfix
|
||||
fields, pattern = routing.compile_uri_template(path)
|
||||
self.assertEqual(fields, set(['name', 'id']))
|
||||
|
||||
result = pattern.match('/book/0765350386/characters/Vin')
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(result.groupdict(), {'name': 'Vin', 'id': '0765350386'})
|
||||
|
||||
def test_three_fields(self):
|
||||
fields, pattern = routing.compile_uri_template('/{a}/{b}/x/{c}')
|
||||
self.assertEqual(fields, set('abc'))
|
||||
|
||||
result = pattern.match('/one/2/x/3')
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(result.groupdict(), {'a': 'one', 'b': '2', 'c': '3'})
|
||||
|
||||
def test_malformed_field(self):
|
||||
fields, pattern = routing.compile_uri_template('/{a}/{1b}/x/{c}')
|
||||
self.assertEqual(fields, set('ac'))
|
||||
|
||||
result = pattern.match('/one/{1b}/x/3')
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(result.groupdict(), {'a': 'one', 'c': '3'})
|
||||
Reference in New Issue
Block a user