diff --git a/sanic/sanic.py b/sanic/sanic.py index 4ad4734a..079049ca 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -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 diff --git a/sanic/views.py b/sanic/views.py index 5d8e9d40..d6da9145 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -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) + return handler(request, *args, **kwargs) @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) diff --git a/tests/test_views.py b/tests/test_views.py index 4e5b17f0..ec0b91b8 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -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,65 @@ 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.' + + +@pytest.mark.parametrize('method', HTTP_METHODS) +def test_composition_view_runs_methods_as_expected(method): + 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, '/') + + if method in ['GET', 'POST', 'PUT']: + request, response = sanic_endpoint_test(app, uri='/', method=method) + assert response.text == 'first method' + + if method in ['DELETE', 'PATCH']: + request, response = sanic_endpoint_test(app, uri='/', method=method) + assert response.text == 'second method' + + +@pytest.mark.parametrize('method', HTTP_METHODS) +def test_composition_view_rejects_invalid_methods(method): + app = Sanic('test_composition_view') + + view = CompositionView() + view.add(['GET', 'POST', 'PUT'], lambda x: text('first method')) + + app.add_route(view, '/') + + if method in ['GET', 'POST', 'PUT']: + request, response = sanic_endpoint_test(app, uri='/', method=method) + assert response.status == 200 + assert response.text == 'first method' + + if method in ['DELETE', 'PATCH']: + request, response = sanic_endpoint_test(app, uri='/', method=method) + assert response.status == 405