Skip to content

Commit

Permalink
Merge d002b5c into 6722c38
Browse files Browse the repository at this point in the history
  • Loading branch information
richardolsson committed Dec 22, 2014
2 parents 6722c38 + d002b5c commit 577554f
Show file tree
Hide file tree
Showing 4 changed files with 325 additions and 70 deletions.
44 changes: 24 additions & 20 deletions falcon/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

import re
import six

from falcon import api_helpers as helpers
from falcon import DEFAULT_MEDIA_TYPE
Expand Down Expand Up @@ -135,13 +136,12 @@ def process_response(req, resp)
_STREAM_BLOCK_SIZE = 8 * 1024 # 8 KiB

__slots__ = ('_after', '_before', '_request_type', '_response_type',
'_error_handlers', '_media_type', '_routes', '_sinks',
'_error_handlers', '_media_type', '_router', '_sinks',
'_serialize_error', 'req_options', '_middleware')

def __init__(self, media_type=DEFAULT_MEDIA_TYPE, before=None, after=None,
request_type=Request, response_type=Response,
middleware=None):
self._routes = []
middleware=None, router=None):
self._sinks = []
self._media_type = media_type

Expand All @@ -151,6 +151,8 @@ def __init__(self, media_type=DEFAULT_MEDIA_TYPE, before=None, after=None,
# set middleware
self._middleware = helpers.prepare_middleware(middleware)

self._router = router or routing.DefaultRouter()

self._request_type = request_type
self._response_type = response_type

Expand Down Expand Up @@ -304,13 +306,20 @@ def on_put(self, req, resp, thing):
"""

uri_fields, path_template = routing.compile_uri_template(uri_template)
method_map = routing.create_http_method_map(
resource, uri_fields, self._before, self._after)
# NOTE(richardolsson): Doing the validation here means it doesn't have
# to be duplicated in every future router implementation.
if not isinstance(uri_template, six.string_types):
raise TypeError('uri_template is not a string')

# Insert at the head of the list in case we get duplicate
# adds (will cause the last one to win).
self._routes.insert(0, (path_template, method_map, resource))
if not uri_template.startswith('/'):
raise ValueError("uri_template must start with '/'")

if '//' in uri_template:
raise ValueError("uri_template may not contain '//'")

method_map = routing.create_http_method_map(
resource, self._before, self._after)
self._router.add_route(uri_template, method_map, resource)

def add_sink(self, sink, prefix=r'/'):
"""Adds a "sink" responder to the API.
Expand Down Expand Up @@ -444,17 +453,12 @@ def _get_responder(self, req):

path = req.path
method = req.method
for path_template, method_map, resource in self._routes:
m = path_template.match(path)
if m:
params = m.groupdict()

try:
responder = method_map[method]
except KeyError:
responder = falcon.responders.bad_request

break
resource, method_map, params = self._router.find(path)
if resource is not None:
try:
responder = method_map[method]
except KeyError:
responder = falcon.responders.bad_request
else:
params = {}
resource = None
Expand Down
53 changes: 3 additions & 50 deletions falcon/routing.py → falcon/routing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,61 +12,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import re

import six

from falcon.hooks import _wrap_with_hooks
from falcon import HTTP_METHODS, responders

from .compiled import CompiledRouter

# 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 currently only recognizes Level 1 URI templates, and only
for the path portion of the URI.
See also: http://tools.ietf.org/html/rfc6570
Args:
template: A Level 1 URI template. Method responders must accept, as
arguments, all fields specified in the template (default '/').
Note that field names are restricted to ASCII a-z, A-Z, and
the underscore '_'.
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)
DefaultRouter = CompiledRouter


# NOTE(kgriffs): Published method; take care to avoid breaking changes.
def create_http_method_map(resource, uri_fields, before, after):
def create_http_method_map(resource, before, after):
"""Maps HTTP methods (e.g., GET, POST) to methods of a resource object.
Args:
Expand All @@ -75,9 +31,6 @@ def create_http_method_map(resource, uri_fields, before, after):
supports. For example, if a resource supports GET and POST, it
should define ``on_get(self, req, resp)`` and
``on_post(self, req, resp)``.
uri_fields: A set of field names from the route's URI template
that a responder must support in order to avoid "method not
allowed".
before: An action hook or list of hooks to be called before each
*on_\** responder defined by the resource.
after: An action hook or list of hooks to be called after each
Expand Down
220 changes: 220 additions & 0 deletions falcon/routing/compiled.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
# Copyright 2013 by Rackspace Hosting, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import re


class CompiledRouter(object):
"""Fast URI router which compiles it's routing logic to Python code.
This class is a Falcon router, which handles the routing from URI paths
to resource class instance methods. It implements the necessary router
methods add_route() and find(). Generally you do not need to use this
router class directly, as an instance is created by default when the
falcon.API class is initialized.
The router treats URI paths as a tree of URI segments and searches by
comparing a URI one segment at a time. Instead of interpreting the route
tree for each look-up, it generates inlined, bespoke Python code to
perform the search and compiles it, making it blazingly fast.
The generated code for the test() method looks something like this:
def test(path, return_values, expressions, params):
path_len = len(path)
if path_len > 0 and path[0] == "books":
if path_len > 1:
params["book_id"] = path[1]
return return_values[1]
return return_values[0]
if path_len > 0 and path[0] == "authors"
if path_len > 1:
params["author_id"] = path[1]
if path_len > 2:
match = expressions[0].search(path[2])
if match is not None:
params.update(match.groupdict())
return return_values[4]
return return_values[3]
return return_values[2]
"""

def __init__(self):
self._roots = []
self._find = None
self._code_lines = None
self._expressions = None
self._return_values = None

def add_route(self, uri_template, method_map, resource):
"""Adds a route between URI path template and resource."""
path = uri_template.strip('/').split('/')

# Reset compiled code
self._find = None

def insert(nodes, path_index=0):
for node in nodes:
if node.matches(path[path_index]):
path_index += 1
if path_index == len(path):
node.method_map = method_map
node.resource = resource
else:
insert(node.children, path_index)

return

# NOTE(richardolsson): If we got this far, the node doesn't already
# exist and needs to be created. This builds a new branch of the
# routing tree recursively until it reaches the new node leaf.
new_node = CompiledRouterNode(path[path_index])
nodes.append(new_node)
if path_index == len(path)-1:
new_node.method_map = method_map
new_node.resource = resource
else:
insert(new_node.children, path_index+1)

insert(self._roots)

def find(self, uri):
"""Finds resource and method map for a URI, or returns None."""
if self._find is None:
self._compile()

path = uri.lstrip('/').split('/')
params = {}
node = self._find(path, self._return_values, self._expressions, params)

if node is not None:
return node.resource, node.method_map, params
else:
return None, None, None

def _compile_node(self, node=None, pad=' ', level=0):
"""Generates Python code for a router node (and it's children)."""
def line(pad, lstr):
self._code_lines.append(pad + lstr)

if node.is_var:
line(pad, 'if path_len > %d:' % level)
if node.is_complex:
# NOTE(richardolsson): Complex nodes are nodes which contain
# anything more than a single literal or variable, and they
# need to be checked using a pre-compiled regular expression.
expression_idx = len(self._expressions)
self._expressions.append(node.var_regex)
line(pad, ' match = expressions[%d].search(path[%d]) # %s' % (
expression_idx, level, node.var_regex.pattern))

line(pad, ' if match is not None:')
line(pad, ' params.update(match.groupdict())')
pad += ' '
else:
line(pad, ' params["%s"] = path[%d]' % (node.var_name, level))
else:
line(pad, 'if path_len > %d and path[%d] == "%s":' % (
level, level, node.raw_segment))

if node.resource is not None:
resource_idx = len(self._return_values)
self._return_values.append(node)

if len(node.children):
for child in node.children:
self._compile_node(child, pad+' ', level+1)
if node.resource is not None:
line(pad, ' return return_values[%d]' % resource_idx)

def _compile(self):
"""Generates Python code for entire routing tree.
The generated code is compiled and the resulting Python method is
stored in the _find member.
"""
self._return_values = []
self._expressions = []
self._code_lines = [
'def find(path, return_values, expressions, params):',
' path_len = len(path)',
]

for root in self._roots:
self._compile_node(root)

src = '\n'.join(self._code_lines)

scope = {}
exec(compile(src, '<string>', 'exec'), scope)
self._find = scope['find']


class CompiledRouterNode(object):
"""Represents a single URI segment in a URI."""

def __init__(self, raw_segment, method_map=None, resource=None):
self.children = []

self.raw_segment = raw_segment
self.method_map = method_map
self.resource = resource

seg = raw_segment.replace('.', '\\.')
matches = list(re.finditer('{([-_a-zA-Z0-9]*)}', seg))
if matches:
self.is_var = True
# NOTE(richardolsson): if there is a single variable and it spans
# the entire segment, the segment is uncomplex and the variable
# name is simply the string contained within curly braces.
if len(matches) == 1 and matches[0].span() == (0, len(seg)):
self.is_complex = False
self.var_name = raw_segment[1:-1]
else:
# NOTE(richardolsson): Complex segments need to be converted
# into regular expressions will be used to match and extract
# variable values. The regular expressions contain both
# literal spans and named group expressions for the variables.
self.is_complex = True
seg_fields = []
prev_end_idx = 0
for match in matches:
var_start_idx, var_end_idx = match.span()
seg_fields.append(seg[prev_end_idx:var_start_idx])
var_name = match.groups()[0].replace('-', '_')
seg_fields.append('(?P<%s>[^/]*)' % var_name)
prev_end_idx = var_end_idx

seg_fields.append(seg[prev_end_idx:])
seg_pattern = ''.join(seg_fields)
self.var_regex = re.compile(seg_pattern)
else:
self.is_var = False

def matches(self, segment):
"""Returns True if this node matches the supplied URI segment."""

if self.is_var:
if self.is_complex:
match = self.var_regex.search(segment)
if match:
return True
else:
return False
else:
return True
elif segment == self.raw_segment:
return True
else:
return False

0 comments on commit 577554f

Please sign in to comment.