This commit is contained in:
Suby Raman 2017-02-02 16:24:16 -05:00
parent 48aa51b739
commit d614823013
4 changed files with 107 additions and 20 deletions

View File

@ -4,8 +4,7 @@ from functools import lru_cache
from .exceptions import NotFound, InvalidUsage from .exceptions import NotFound, InvalidUsage
from .views import CompositionView from .views import CompositionView
Route = namedtuple( Route = namedtuple('Route',
'Route',
['handler', 'methods', 'pattern', 'parameters', 'name']) ['handler', 'methods', 'pattern', 'parameters', 'name'])
Parameter = namedtuple('Parameter', ['name', 'cast']) Parameter = namedtuple('Parameter', ['name', 'cast'])
@ -70,6 +69,28 @@ class Router:
self.routes_always_check = [] self.routes_always_check = []
self.hosts = None self.hosts = None
def __str__(self):
"""
The typical user inspecting the router will likely want to see
the routes available. Provide a simple representation.
"""
def _route_to_str(uri, route):
out = 'name={0.name}, methods={0.methods}, URI={1}>\n'.format(
route, uri)
if route.handler.__doc__:
out += '{}\n'.format(route.handler.__doc__)
out += '\n'
return out
out = ''
for uri, route in self.routes_all.items():
out += _route_to_str(uri, route)
return out
def parse_parameter_string(self, parameter_string): def parse_parameter_string(self, parameter_string):
""" """
Parse a parameter string into its constituent name, type, and pattern Parse a parameter string into its constituent name, type, and pattern
@ -130,11 +151,16 @@ class Router:
properties = {"unhashable": None} properties = {"unhashable": None}
def add_parameter(match): def add_parameter(match):
# We could receive NAME or NAME:PATTERN
name = match.group(1) name = match.group(1)
name, _type, pattern = self.parse_parameter_string(name) pattern = 'string'
if ':' in name:
name, pattern = name.split(':', 1)
parameter = Parameter( default = (str, pattern)
name=name, cast=_type) # Pull from pre-configured types
_type, pattern = REGEX_TYPES.get(pattern, default)
parameter = Parameter(name=name, cast=_type)
parameters.append(parameter) parameters.append(parameter)
# Mark the whole route as unhashable if it has the hash key in it # Mark the whole route as unhashable if it has the hash key in it
@ -146,7 +172,7 @@ class Router:
return '({})'.format(pattern) return '({})'.format(pattern)
pattern_string = re.sub(self.parameter_pattern, add_parameter, uri) pattern_string = re.sub(r'<(.+?)>', add_parameter, uri)
pattern = re.compile(r'^{}$'.format(pattern_string)) pattern = re.compile(r'^{}$'.format(pattern_string))
def merge_route(route, methods, handler): def merge_route(route, methods, handler):

View File

@ -17,6 +17,7 @@ from .response import HTTPResponse
from .router import Router from .router import Router
from .server import serve, serve_multiple, HttpProtocol from .server import serve, serve_multiple, HttpProtocol
from .static import register as static_register from .static import register as static_register
from .views import CompositionView
class Sanic: class Sanic:
@ -120,7 +121,16 @@ class Sanic:
""" """
# Handle HTTPMethodView differently # Handle HTTPMethodView differently
if hasattr(handler, 'view_class'): if hasattr(handler, 'view_class'):
methods = frozenset(HTTP_METHODS) methods = set()
for method in HTTP_METHODS:
if getattr(handler.view_class, method.lower(), None):
methods.add(method)
# handle composition view differently
if isinstance(handler, CompositionView):
methods = handler.handlers.keys()
self.route(uri=uri, methods=methods, host=host)(handler) self.route(uri=uri, methods=methods, host=host)(handler)
return handler return handler

View File

@ -1,4 +1,5 @@
from .exceptions import InvalidUsage from .exceptions import InvalidUsage
from .constants import HTTP_METHODS
class HTTPMethodView: class HTTPMethodView:
@ -40,11 +41,7 @@ class HTTPMethodView:
def dispatch_request(self, request, *args, **kwargs): def dispatch_request(self, request, *args, **kwargs):
handler = getattr(self, request.method.lower(), None) handler = getattr(self, request.method.lower(), None)
if handler: return handler(request, *args, **kwargs)
return handler(request, *args, **kwargs)
raise InvalidUsage(
'Method {} not allowed for URL {}'.format(
request.method, request.url), status_code=405)
@classmethod @classmethod
def as_view(cls, *class_args, **class_kwargs): def as_view(cls, *class_args, **class_kwargs):
@ -89,15 +86,15 @@ class CompositionView:
def add(self, methods, handler): def add(self, methods, handler):
for method in methods: for method in methods:
if method not in HTTP_METHODS:
raise InvalidUsage(
'{} is not a valid HTTP method.'.format(method))
if method in self.handlers: if method in self.handlers:
raise KeyError( raise InvalidUsage(
'Method {} already is registered.'.format(method)) 'Method {} is already registered.'.format(method))
self.handlers[method] = handler self.handlers[method] = handler
def __call__(self, request, *args, **kwargs): def __call__(self, request, *args, **kwargs):
handler = self.handlers.get(request.method.upper(), None) handler = self.handlers[request.method.upper()]
if handler is None:
raise InvalidUsage(
'Method {} not allowed for URL {}'.format(
request.method, request.url), status_code=405)
return handler(request, *args, **kwargs) return handler(request, *args, **kwargs)

View File

@ -1,8 +1,9 @@
import pytest as pytest import pytest as pytest
from sanic import Sanic from sanic import Sanic
from sanic.exceptions import InvalidUsage
from sanic.response import text, HTTPResponse from sanic.response import text, HTTPResponse
from sanic.views import HTTPMethodView from sanic.views import HTTPMethodView, CompositionView
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.request import Request from sanic.request import Request
from sanic.utils import sanic_endpoint_test from sanic.utils import sanic_endpoint_test
@ -196,3 +197,56 @@ def test_with_decorator():
request, response = sanic_endpoint_test(app, method="get") request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method' assert response.text == 'I am get method'
assert results[0] == 1 assert results[0] == 1
def test_composition_view_rejects_incorrect_methods():
def foo(request):
return text('Foo')
view = CompositionView()
with pytest.raises(InvalidUsage) as e:
view.add(['GET', 'FOO'], foo)
assert str(e.value) == 'FOO is not a valid HTTP method.'
def test_composition_view_rejects_duplicate_methods():
def foo(request):
return text('Foo')
view = CompositionView()
with pytest.raises(InvalidUsage) as e:
view.add(['GET', 'POST', 'GET'], foo)
assert str(e.value) == 'Method GET is already registered.'
def test_composition_view_runs_methods_as_expected():
app = Sanic('test_composition_view')
view = CompositionView()
view.add(['GET', 'POST', 'PUT'], lambda x: text('first method'))
view.add(['DELETE', 'PATCH'], lambda x: text('second method'))
app.add_route(view, '/')
for method in ['GET', 'POST', 'PUT']:
request, response = sanic_endpoint_test(app, uri='/', method=method)
assert response.text == 'first method'
for method in ['DELETE', 'PATCH']:
request, response = sanic_endpoint_test(app, uri='/', method=method)
assert response.text == 'second method'
def test_composition_view_rejects_invalid_methods():
app = Sanic('test_composition_view')
view = CompositionView()
view.add(['GET', 'POST', 'PUT'], lambda x: text('first method'))
app.add_route(view, '/')
for method in ['DELETE', 'PATCH']:
request, response = sanic_endpoint_test(app, uri='/', method=method)
assert response.status == 405