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 .views import CompositionView
Route = namedtuple(
'Route',
Route = namedtuple('Route',
['handler', 'methods', 'pattern', 'parameters', 'name'])
Parameter = namedtuple('Parameter', ['name', 'cast'])
@ -70,6 +69,28 @@ class Router:
self.routes_always_check = []
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):
"""
Parse a parameter string into its constituent name, type, and pattern
@ -130,11 +151,16 @@ class Router:
properties = {"unhashable": None}
def add_parameter(match):
# We could receive NAME or NAME:PATTERN
name = match.group(1)
name, _type, pattern = self.parse_parameter_string(name)
pattern = 'string'
if ':' in name:
name, pattern = name.split(':', 1)
parameter = Parameter(
name=name, cast=_type)
default = (str, pattern)
# Pull from pre-configured types
_type, pattern = REGEX_TYPES.get(pattern, default)
parameter = Parameter(name=name, cast=_type)
parameters.append(parameter)
# Mark the whole route as unhashable if it has the hash key in it
@ -146,7 +172,7 @@ class Router:
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))
def merge_route(route, methods, handler):

View File

@ -17,6 +17,7 @@ from .response import HTTPResponse
from .router import Router
from .server import serve, serve_multiple, HttpProtocol
from .static import register as static_register
from .views import CompositionView
class Sanic:
@ -120,7 +121,16 @@ class Sanic:
"""
# Handle HTTPMethodView differently
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)
return handler

View File

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

View File

@ -1,8 +1,9 @@
import pytest as pytest
from sanic import Sanic
from sanic.exceptions import InvalidUsage
from sanic.response import text, HTTPResponse
from sanic.views import HTTPMethodView
from sanic.views import HTTPMethodView, CompositionView
from sanic.blueprints import Blueprint
from sanic.request import Request
from sanic.utils import sanic_endpoint_test
@ -196,3 +197,56 @@ def test_with_decorator():
request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method'
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