From 9f2d73e2f152456df27941efc3d5fe2695e0af6b Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Fri, 25 Nov 2016 09:10:25 +0200 Subject: [PATCH 01/16] class based views implementation for sanic --- docs/class_based_views.md | 44 +++++++++ docs/routing.md | 12 +++ requirements-dev.txt | 1 + sanic/blueprints.py | 6 ++ sanic/sanic.py | 11 +++ sanic/utils.py | 4 +- sanic/views.py | 33 +++++++ tests/test_routes.py | 184 +++++++++++++++++++++++++++++++++++++- tests/test_views.py | 155 ++++++++++++++++++++++++++++++++ 9 files changed, 445 insertions(+), 5 deletions(-) create mode 100644 docs/class_based_views.md create mode 100644 sanic/views.py create mode 100644 tests/test_views.py diff --git a/docs/class_based_views.md b/docs/class_based_views.md new file mode 100644 index 00000000..b5f8ee02 --- /dev/null +++ b/docs/class_based_views.md @@ -0,0 +1,44 @@ +# Class based views + +Sanic has simple class based implementation. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. If someone try to use not implemented method, there will be 405 response. + +## Examples +```python +from sanic import Sanic +from sanic.views import MethodView + +app = Sanic('some_name') + + +class SimpleView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + def post(self, request, *args, **kwargs): + return text('I am post method') + + def put(self, request, *args, **kwargs): + return text('I am put method') + + def patch(self, request, *args, **kwargs): + return text('I am patch method') + + def delete(self, request, *args, **kwargs): + return text('I am delete method') + +app.add_route(SimpleView(), '/') + +``` + +If you need any url params just mention them in method definition: + +```python +class NameView(MethodView): + + def get(self, request, name, *args, **kwargs): + return text('Hello {}'.format(name)) + +app.add_route(NameView(), '/') + +async def person_handler(request, name): + return text('Person - {}'.format(name)) +app.add_route(handler, '/person/') + ``` diff --git a/requirements-dev.txt b/requirements-dev.txt index 00feb17d..1c34d695 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,3 +11,4 @@ bottle kyoukai falcon tornado +aiofiles diff --git a/sanic/blueprints.py b/sanic/blueprints.py index bfef8557..92e376f1 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -91,6 +91,12 @@ class Blueprint: return handler return decorator + def add_route(self, handler, uri, methods=None): + """ + """ + self.record(lambda s: s.add_route(handler, uri, methods)) + return handler + def listener(self, event): """ """ diff --git a/sanic/sanic.py b/sanic/sanic.py index af284c00..7a9c35ad 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -60,6 +60,17 @@ class Sanic: return response + def add_route(self, handler, uri, methods=None): + """ + A helper method to register class instance or functions as a handler to the application url routes. + :param handler: function or class instance + :param uri: path of the URL + :param methods: list or tuple of methods allowed + :return: function or class instance + """ + self.route(uri=uri, methods=methods)(handler) + return handler + # Decorator def exception(self, *exceptions): """ diff --git a/sanic/utils.py b/sanic/utils.py index 0749464b..8190c1d0 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -16,7 +16,7 @@ async def local_request(method, uri, cookies=None, *args, **kwargs): def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, - loop=None, *request_args, **request_kwargs): + loop=None, debug=False, *request_args, **request_kwargs): results = [] exceptions = [] @@ -34,7 +34,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, exceptions.append(e) app.stop() - app.run(host=HOST, port=42101, after_start=_collect_response, loop=loop) + app.run(host=HOST, debug=debug, port=42101, after_start=_collect_response, loop=loop) if exceptions: raise ValueError("Exception during request: {}".format(exceptions)) diff --git a/sanic/views.py b/sanic/views.py new file mode 100644 index 00000000..9cb04247 --- /dev/null +++ b/sanic/views.py @@ -0,0 +1,33 @@ +from .exceptions import InvalidUsage + + +class MethodView: + """ Simple class based implementation of view for the sanic. + You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. + For example: + class DummyView(View): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + def put(self, request, *args, **kwargs): + return text('I am put method') + etc. + If someone try use not implemented method, there will be 405 response + + If you need any url params just mention them in method definition like: + class DummyView(View): + + def get(self, request, my_param_here, *args, **kwargs): + return text('I am get method with %s' % my_param_here) + + To add the view into the routing you could use + 1) app.add_route(DummyView(), '/') + 2) app.route('/')(DummyView()) + """ + + def __call__(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) diff --git a/tests/test_routes.py b/tests/test_routes.py index 8b0fd9f6..38591e53 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -84,7 +84,7 @@ def test_dynamic_route_int(): def test_dynamic_route_number(): - app = Sanic('test_dynamic_route_int') + app = Sanic('test_dynamic_route_number') results = [] @@ -105,7 +105,7 @@ def test_dynamic_route_number(): def test_dynamic_route_regex(): - app = Sanic('test_dynamic_route_int') + app = Sanic('test_dynamic_route_regex') @app.route('/folder/') async def handler(request, folder_id): @@ -145,7 +145,7 @@ def test_dynamic_route_unhashable(): def test_route_duplicate(): - app = Sanic('test_dynamic_route') + app = Sanic('test_route_duplicate') with pytest.raises(RouteExists): @app.route('/test') @@ -178,3 +178,181 @@ def test_method_not_allowed(): request, response = sanic_endpoint_test(app, method='post', uri='/test') assert response.status == 405 + + +def test_static_add_route(): + app = Sanic('test_static_add_route') + + async def handler1(request): + return text('OK1') + + async def handler2(request): + return text('OK2') + + app.add_route(handler1, '/test') + app.add_route(handler2, '/test2') + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.text == 'OK1' + + request, response = sanic_endpoint_test(app, uri='/test2') + assert response.text == 'OK2' + + +def test_dynamic_add_route(): + app = Sanic('test_dynamic_add_route') + + results = [] + + async def handler(request, name): + results.append(name) + return text('OK') + + app.add_route(handler, '/folder/') + request, response = sanic_endpoint_test(app, uri='/folder/test123') + + assert response.text == 'OK' + assert results[0] == 'test123' + + +def test_dynamic_add_route_string(): + app = Sanic('test_dynamic_add_route_string') + + results = [] + + async def handler(request, name): + results.append(name) + return text('OK') + + app.add_route(handler, '/folder/') + request, response = sanic_endpoint_test(app, uri='/folder/test123') + + assert response.text == 'OK' + assert results[0] == 'test123' + + request, response = sanic_endpoint_test(app, uri='/folder/favicon.ico') + + assert response.text == 'OK' + assert results[1] == 'favicon.ico' + + +def test_dynamic_add_route_int(): + app = Sanic('test_dynamic_add_route_int') + + results = [] + + async def handler(request, folder_id): + results.append(folder_id) + return text('OK') + + app.add_route(handler, '/folder/') + + request, response = sanic_endpoint_test(app, uri='/folder/12345') + assert response.text == 'OK' + assert type(results[0]) is int + + request, response = sanic_endpoint_test(app, uri='/folder/asdf') + assert response.status == 404 + + +def test_dynamic_add_route_number(): + app = Sanic('test_dynamic_add_route_number') + + results = [] + + async def handler(request, weight): + results.append(weight) + return text('OK') + + app.add_route(handler, '/weight/') + + request, response = sanic_endpoint_test(app, uri='/weight/12345') + assert response.text == 'OK' + assert type(results[0]) is float + + request, response = sanic_endpoint_test(app, uri='/weight/1234.56') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/weight/1234-56') + assert response.status == 404 + + +def test_dynamic_add_route_regex(): + app = Sanic('test_dynamic_route_int') + + async def handler(request, folder_id): + return text('OK') + + app.add_route(handler, '/folder/') + + request, response = sanic_endpoint_test(app, uri='/folder/test') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/folder/test1') + assert response.status == 404 + + request, response = sanic_endpoint_test(app, uri='/folder/test-123') + assert response.status == 404 + + request, response = sanic_endpoint_test(app, uri='/folder/') + assert response.status == 200 + + +def test_dynamic_add_route_unhashable(): + app = Sanic('test_dynamic_add_route_unhashable') + + async def handler(request, unhashable): + return text('OK') + + app.add_route(handler, '/folder//end/') + + request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/folder/test///////end/') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/folder/test/end/') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/folder/test/nope/') + assert response.status == 404 + + +def test_add_route_duplicate(): + app = Sanic('test_add_route_duplicate') + + with pytest.raises(RouteExists): + async def handler1(request): + pass + + async def handler2(request): + pass + + app.add_route(handler1, '/test') + app.add_route(handler2, '/test') + + with pytest.raises(RouteExists): + async def handler1(request, dynamic): + pass + + async def handler2(request, dynamic): + pass + + app.add_route(handler1, '/test//') + app.add_route(handler2, '/test//') + + +def test_add_route_method_not_allowed(): + app = Sanic('test_add_route_method_not_allowed') + + async def handler(request): + return text('OK') + + app.add_route(handler, '/test', methods=['GET']) + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, method='post', uri='/test') + assert response.status == 405 diff --git a/tests/test_views.py b/tests/test_views.py new file mode 100644 index 00000000..251b7a10 --- /dev/null +++ b/tests/test_views.py @@ -0,0 +1,155 @@ +from sanic import Sanic +from sanic.response import text, HTTPResponse +from sanic.views import MethodView +from sanic.blueprints import Blueprint +from sanic.request import Request +from sanic.utils import sanic_endpoint_test + + +def test_methods(): + app = Sanic('test_methods') + + class DummyView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + def post(self, request, *args, **kwargs): + return text('I am post method') + + def put(self, request, *args, **kwargs): + return text('I am put method') + + def patch(self, request, *args, **kwargs): + return text('I am patch method') + + def delete(self, request, *args, **kwargs): + return text('I am delete method') + + app.add_route(DummyView(), '/') + + request, response = sanic_endpoint_test(app, method="get") + assert response.text == 'I am get method' + request, response = sanic_endpoint_test(app, method="post") + assert response.text == 'I am post method' + request, response = sanic_endpoint_test(app, method="put") + assert response.text == 'I am put method' + request, response = sanic_endpoint_test(app, method="patch") + assert response.text == 'I am patch method' + request, response = sanic_endpoint_test(app, method="delete") + assert response.text == 'I am delete method' + + +def test_unexisting_methods(): + app = Sanic('test_unexisting_methods') + + class DummyView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + app.add_route(DummyView(), '/') + request, response = sanic_endpoint_test(app, method="get") + assert response.text == 'I am get method' + request, response = sanic_endpoint_test(app, method="post") + assert response.text == 'Error: Method POST not allowed for URL /' + + +def test_argument_methods(): + app = Sanic('test_argument_methods') + + class DummyView(MethodView): + + def get(self, request, my_param_here, *args, **kwargs): + return text('I am get method with %s' % my_param_here) + + app.add_route(DummyView(), '/') + + request, response = sanic_endpoint_test(app, uri='/test123') + + assert response.text == 'I am get method with test123' + + +def test_with_bp(): + app = Sanic('test_with_bp') + bp = Blueprint('test_text') + + class DummyView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + bp.add_route(DummyView(), '/') + + app.blueprint(bp) + request, response = sanic_endpoint_test(app) + + assert response.text == 'I am get method' + + +def test_with_bp_with_url_prefix(): + app = Sanic('test_with_bp_with_url_prefix') + bp = Blueprint('test_text', url_prefix='/test1') + + class DummyView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + bp.add_route(DummyView(), '/') + + app.blueprint(bp) + request, response = sanic_endpoint_test(app, uri='/test1/') + + assert response.text == 'I am get method' + + +def test_with_middleware(): + app = Sanic('test_with_middleware') + + class DummyView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + app.add_route(DummyView(), '/') + + results = [] + + @app.middleware + async def handler(request): + results.append(request) + + request, response = sanic_endpoint_test(app) + + assert response.text == 'I am get method' + assert type(results[0]) is Request + + +def test_with_middleware_response(): + app = Sanic('test_with_middleware_response') + + results = [] + + @app.middleware('request') + async def process_response(request): + results.append(request) + + @app.middleware('response') + async def process_response(request, response): + results.append(request) + results.append(response) + + class DummyView(MethodView): + + def get(self, request, *args, **kwargs): + return text('I am get method') + + app.add_route(DummyView(), '/') + + request, response = sanic_endpoint_test(app) + + assert response.text == 'I am get method' + assert type(results[0]) is Request + assert type(results[1]) is Request + assert issubclass(type(results[2]), HTTPResponse) From fca0221d911dac1f53dd94b60ed51f06835472b8 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Fri, 25 Nov 2016 09:14:37 +0200 Subject: [PATCH 02/16] update readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b6d87de..2ece97d8 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ app.run(host="0.0.0.0", port=8000) * [Middleware](docs/middleware.md) * [Exceptions](docs/exceptions.md) * [Blueprints](docs/blueprints.md) + * [Class Based Views](docs/class_based_views.md) * [Cookies](docs/cookies.md) * [Static Files](docs/static_files.md) * [Deploying](docs/deploying.md) @@ -72,7 +73,7 @@ app.run(host="0.0.0.0", port=8000) ▄▄▄▄▄ ▀▀▀██████▄▄▄ _______________ ▄▄▄▄▄ █████████▄ / \ - ▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! | + ▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! | ▀▀█████▄▄ ▀██████▄██ | _________________/ ▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/ ▀▀▀▄ ▀▀███ ▀ ▄▄ From c3c7964e2e49fc5bac044551700365eeaf8ba006 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Fri, 25 Nov 2016 09:29:25 +0200 Subject: [PATCH 03/16] pep8 fixes --- sanic/sanic.py | 4 +++- sanic/utils.py | 6 ++++-- sanic/views.py | 7 +++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 7a9c35ad..33e16af7 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -62,7 +62,9 @@ class Sanic: def add_route(self, handler, uri, methods=None): """ - A helper method to register class instance or functions as a handler to the application url routes. + A helper method to register class instance or + functions as a handler to the application url + routes. :param handler: function or class instance :param uri: path of the URL :param methods: list or tuple of methods allowed diff --git a/sanic/utils.py b/sanic/utils.py index 8190c1d0..5d896312 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -16,7 +16,8 @@ async def local_request(method, uri, cookies=None, *args, **kwargs): def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, - loop=None, debug=False, *request_args, **request_kwargs): + loop=None, debug=False, *request_args, + **request_kwargs): results = [] exceptions = [] @@ -34,7 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, exceptions.append(e) app.stop() - app.run(host=HOST, debug=debug, port=42101, after_start=_collect_response, loop=loop) + app.run(host=HOST, debug=debug, port=42101, + after_start=_collect_response, loop=loop) if exceptions: raise ValueError("Exception during request: {}".format(exceptions)) diff --git a/sanic/views.py b/sanic/views.py index 9cb04247..2c4dcce2 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -3,7 +3,8 @@ from .exceptions import InvalidUsage class MethodView: """ Simple class based implementation of view for the sanic. - You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. + You should implement methods(get, post, put, patch, delete) for the class + to every HTTP method you want to support. For example: class DummyView(View): @@ -30,4 +31,6 @@ class MethodView: 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) + raise InvalidUsage( + 'Method {} not allowed for URL {}'.format( + request.method, request.url), status_code=405) From 0ca5c4eeff5e4eab6f986703acc7f3b7f3ef18c2 Mon Sep 17 00:00:00 2001 From: John Piasetzki Date: Fri, 25 Nov 2016 14:33:17 -0500 Subject: [PATCH 04/16] Use explicit import for httptools Explicit importing the parser and the exception to save a name lookup. --- sanic/server.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index b6233031..6301d18f 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -4,7 +4,8 @@ from inspect import isawaitable from multidict import CIMultiDict from signal import SIGINT, SIGTERM from time import time -import httptools +from httptools import HttpRequestParser +from httptools.parser.errors import HttpParserError try: import uvloop as async_loop @@ -94,12 +95,12 @@ class HttpProtocol(asyncio.Protocol): if self.parser is None: assert self.request is None self.headers = [] - self.parser = httptools.HttpRequestParser(self) + self.parser = HttpRequestParser(self) # Parse request chunk or close connection try: self.parser.feed_data(data) - except httptools.parser.errors.HttpParserError as e: + except HttpParserError as e: self.bail_out( "Invalid request data, connection closed ({})".format(e)) From c01cbb3a8c93e90fd22a3a58ec93804ee369a5dc Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 26 Nov 2016 13:55:45 +0900 Subject: [PATCH 05/16] Change Request timeout process This add a request timeout exception. It cancels task, when request is timeout. --- examples/request_timeout.py | 21 ++++++++++++++++++ sanic/exceptions.py | 4 ++++ sanic/sanic.py | 1 + sanic/server.py | 28 ++++++++++++++++++------ tests/test_request_timeout.py | 40 +++++++++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 examples/request_timeout.py create mode 100644 tests/test_request_timeout.py diff --git a/examples/request_timeout.py b/examples/request_timeout.py new file mode 100644 index 00000000..496864cd --- /dev/null +++ b/examples/request_timeout.py @@ -0,0 +1,21 @@ +from sanic import Sanic +import asyncio +from sanic.response import text +from sanic.config import Config +from sanic.exceptions import RequestTimeout + +Config.REQUEST_TIMEOUT = 1 +app = Sanic(__name__) + + +@app.route("/") +async def test(request): + await asyncio.sleep(3) + return text('Hello, world!') + + +@app.exception(RequestTimeout) +def timeout(request, exception): + return text('RequestTimeout from error_handler.') + +app.run(host="0.0.0.0", port=8000) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index e21aca63..bc052fbd 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -30,6 +30,10 @@ class FileNotFound(NotFound): self.relative_url = relative_url +class RequestTimeout(SanicException): + status_code = 408 + + class Handler: handlers = None diff --git a/sanic/sanic.py b/sanic/sanic.py index af284c00..128e3d28 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -250,6 +250,7 @@ class Sanic: 'sock': sock, 'debug': debug, 'request_handler': self.handle_request, + 'error_handler': self.error_handler, 'request_timeout': self.config.REQUEST_TIMEOUT, 'request_max_size': self.config.REQUEST_MAX_SIZE, 'loop': loop diff --git a/sanic/server.py b/sanic/server.py index b6233031..4b804353 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -13,6 +13,8 @@ except ImportError: from .log import log from .request import Request +from .response import HTTPResponse +from .exceptions import RequestTimeout class Signal: @@ -33,8 +35,8 @@ class HttpProtocol(asyncio.Protocol): # connection management '_total_request_size', '_timeout_handler', '_last_communication_time') - def __init__(self, *, loop, request_handler, signal=Signal(), - connections={}, request_timeout=60, + def __init__(self, *, loop, request_handler, error_handler, + signal=Signal(), connections={}, request_timeout=60, request_max_size=None): self.loop = loop self.transport = None @@ -45,11 +47,13 @@ class HttpProtocol(asyncio.Protocol): self.signal = signal self.connections = connections self.request_handler = request_handler + self.error_handler = error_handler self.request_timeout = request_timeout self.request_max_size = request_max_size self._total_request_size = 0 self._timeout_handler = None self._last_request_time = None + self._request_handler_task = None # -------------------------------------------- # # Connection @@ -75,7 +79,17 @@ class HttpProtocol(asyncio.Protocol): self._timeout_handler = \ self.loop.call_later(time_left, self.connection_timeout) else: - self.bail_out("Request timed out, connection closed") + self._request_handler_task.cancel() + try: + response = self.error_handler.response( + self.request, RequestTimeout('Request Timeout')) + except Exception as e: + response = HTTPResponse( + 'Request Timeout', RequestTimeout.status_code) + self.transport.write( + response.output( + self.request.version, False, self.request_timeout)) + self.transport.close() # -------------------------------------------- # # Parsing @@ -132,7 +146,7 @@ class HttpProtocol(asyncio.Protocol): self.request.body = body def on_message_complete(self): - self.loop.create_task( + self._request_handler_task = self.loop.create_task( self.request_handler(self.request, self.write_response)) # -------------------------------------------- # @@ -165,6 +179,7 @@ class HttpProtocol(asyncio.Protocol): self.request = None self.url = None self.headers = None + self._request_handler_task = None self._total_request_size = 0 def close_if_idle(self): @@ -204,8 +219,8 @@ def trigger_events(events, loop): loop.run_until_complete(result) -def serve(host, port, request_handler, before_start=None, after_start=None, - before_stop=None, after_stop=None, +def serve(host, port, request_handler, error_handler, before_start=None, + after_start=None, before_stop=None, after_stop=None, debug=False, request_timeout=60, sock=None, request_max_size=None, reuse_port=False, loop=None): """ @@ -240,6 +255,7 @@ def serve(host, port, request_handler, before_start=None, after_start=None, connections=connections, signal=signal, request_handler=request_handler, + error_handler=error_handler, request_timeout=request_timeout, request_max_size=request_max_size, ), host, port, reuse_port=reuse_port, sock=sock) diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py new file mode 100644 index 00000000..8cf3a680 --- /dev/null +++ b/tests/test_request_timeout.py @@ -0,0 +1,40 @@ +from sanic import Sanic +import asyncio +from sanic.response import text +from sanic.exceptions import RequestTimeout +from sanic.utils import sanic_endpoint_test +from sanic.config import Config + +Config.REQUEST_TIMEOUT = 1 +request_timeout_app = Sanic('test_request_timeout') +request_timeout_default_app = Sanic('test_request_timeout_default') + + +@request_timeout_app.route('/1') +async def handler_1(request): + await asyncio.sleep(2) + return text('OK') + + +@request_timeout_app.exception(RequestTimeout) +def handler_exception(request, exception): + return text('Request Timeout from error_handler.', 408) + + +def test_server_error_request_timeout(): + request, response = sanic_endpoint_test(request_timeout_app, uri='/1') + assert response.status == 408 + assert response.text == 'Request Timeout from error_handler.' + + +@request_timeout_default_app.route('/1') +async def handler_2(request): + await asyncio.sleep(2) + return text('OK') + + +def test_default_server_error_request_timeout(): + request, response = sanic_endpoint_test( + request_timeout_default_app, uri='/1') + assert response.status == 408 + assert response.text == 'Error: Request Timeout' From 0bd61f6a57948c94cb9deff0e378b95fc8f0cb4f Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 26 Nov 2016 14:14:30 +0900 Subject: [PATCH 06/16] Use write_response --- sanic/server.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 4b804353..339b8132 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -86,10 +86,7 @@ class HttpProtocol(asyncio.Protocol): except Exception as e: response = HTTPResponse( 'Request Timeout', RequestTimeout.status_code) - self.transport.write( - response.output( - self.request.version, False, self.request_timeout)) - self.transport.close() + self.write_response(response) # -------------------------------------------- # # Parsing From d8e480ab4889891906807696f36180086f00aa70 Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 26 Nov 2016 14:47:42 +0900 Subject: [PATCH 07/16] Change sleep time --- examples/request_timeout.py | 4 ++-- tests/test_request_timeout.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/request_timeout.py b/examples/request_timeout.py index 496864cd..ddae7688 100644 --- a/examples/request_timeout.py +++ b/examples/request_timeout.py @@ -8,7 +8,7 @@ Config.REQUEST_TIMEOUT = 1 app = Sanic(__name__) -@app.route("/") +@app.route('/') async def test(request): await asyncio.sleep(3) return text('Hello, world!') @@ -18,4 +18,4 @@ async def test(request): def timeout(request, exception): return text('RequestTimeout from error_handler.') -app.run(host="0.0.0.0", port=8000) +app.run(host='0.0.0.0', port=8000) diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index 8cf3a680..7b8cfb21 100644 --- a/tests/test_request_timeout.py +++ b/tests/test_request_timeout.py @@ -12,7 +12,7 @@ request_timeout_default_app = Sanic('test_request_timeout_default') @request_timeout_app.route('/1') async def handler_1(request): - await asyncio.sleep(2) + await asyncio.sleep(1) return text('OK') @@ -29,7 +29,7 @@ def test_server_error_request_timeout(): @request_timeout_default_app.route('/1') async def handler_2(request): - await asyncio.sleep(2) + await asyncio.sleep(1) return text('OK') From 9010a6573fea7f855b0597986248a2f3d79d1ba4 Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 26 Nov 2016 15:21:57 +0900 Subject: [PATCH 08/16] Add status code --- examples/request_timeout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/request_timeout.py b/examples/request_timeout.py index ddae7688..261f423a 100644 --- a/examples/request_timeout.py +++ b/examples/request_timeout.py @@ -16,6 +16,6 @@ async def test(request): @app.exception(RequestTimeout) def timeout(request, exception): - return text('RequestTimeout from error_handler.') + return text('RequestTimeout from error_handler.', 408) app.run(host='0.0.0.0', port=8000) From da4567eea5f84d208f7fbadb1bac7e310297a319 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sat, 26 Nov 2016 08:44:46 +0200 Subject: [PATCH 09/16] changes in doc --- docs/class_based_views.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/class_based_views.md b/docs/class_based_views.md index b5f8ee02..c4ceeb0c 100644 --- a/docs/class_based_views.md +++ b/docs/class_based_views.md @@ -1,30 +1,30 @@ # Class based views -Sanic has simple class based implementation. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. If someone try to use not implemented method, there will be 405 response. +Sanic has simple class based implementation. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. If someone tries to use a method that has not been implemented, there will be 405 response. ## Examples ```python from sanic import Sanic -from sanic.views import MethodView +from sanic.views import HTTPMethodView app = Sanic('some_name') -class SimpleView(MethodView): +class SimpleView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') - def post(self, request, *args, **kwargs): + def post(self, request): return text('I am post method') - def put(self, request, *args, **kwargs): + def put(self, request): return text('I am put method') - def patch(self, request, *args, **kwargs): + def patch(self, request): return text('I am patch method') - def delete(self, request, *args, **kwargs): + def delete(self, request): return text('I am delete method') app.add_route(SimpleView(), '/') @@ -34,9 +34,9 @@ app.add_route(SimpleView(), '/') If you need any url params just mention them in method definition: ```python -class NameView(MethodView): +class NameView(HTTPMethodView): - def get(self, request, name, *args, **kwargs): + def get(self, request, name): return text('Hello {}'.format(name)) app.add_route(NameView(), '/ Date: Sat, 26 Nov 2016 08:45:08 +0200 Subject: [PATCH 10/16] rename&remove redundant code --- sanic/views.py | 2 +- tests/test_views.py | 38 +++++++++++++++++++------------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/sanic/views.py b/sanic/views.py index 2c4dcce2..980a5f74 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -1,7 +1,7 @@ from .exceptions import InvalidUsage -class MethodView: +class HTTPMethodView: """ Simple class based implementation of view for the sanic. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. diff --git a/tests/test_views.py b/tests/test_views.py index 251b7a10..59acb847 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,6 +1,6 @@ from sanic import Sanic from sanic.response import text, HTTPResponse -from sanic.views import MethodView +from sanic.views import HTTPMethodView from sanic.blueprints import Blueprint from sanic.request import Request from sanic.utils import sanic_endpoint_test @@ -9,21 +9,21 @@ from sanic.utils import sanic_endpoint_test def test_methods(): app = Sanic('test_methods') - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') - def post(self, request, *args, **kwargs): + def post(self, request): return text('I am post method') - def put(self, request, *args, **kwargs): + def put(self, request): return text('I am put method') - def patch(self, request, *args, **kwargs): + def patch(self, request): return text('I am patch method') - def delete(self, request, *args, **kwargs): + def delete(self, request): return text('I am delete method') app.add_route(DummyView(), '/') @@ -43,9 +43,9 @@ def test_methods(): def test_unexisting_methods(): app = Sanic('test_unexisting_methods') - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') app.add_route(DummyView(), '/') @@ -58,9 +58,9 @@ def test_unexisting_methods(): def test_argument_methods(): app = Sanic('test_argument_methods') - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, my_param_here, *args, **kwargs): + def get(self, request, my_param_here): return text('I am get method with %s' % my_param_here) app.add_route(DummyView(), '/') @@ -74,9 +74,9 @@ def test_with_bp(): app = Sanic('test_with_bp') bp = Blueprint('test_text') - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') bp.add_route(DummyView(), '/') @@ -91,9 +91,9 @@ def test_with_bp_with_url_prefix(): app = Sanic('test_with_bp_with_url_prefix') bp = Blueprint('test_text', url_prefix='/test1') - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') bp.add_route(DummyView(), '/') @@ -107,9 +107,9 @@ def test_with_bp_with_url_prefix(): def test_with_middleware(): app = Sanic('test_with_middleware') - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') app.add_route(DummyView(), '/') @@ -140,9 +140,9 @@ def test_with_middleware_response(): results.append(request) results.append(response) - class DummyView(MethodView): + class DummyView(HTTPMethodView): - def get(self, request, *args, **kwargs): + def get(self, request): return text('I am get method') app.add_route(DummyView(), '/') From a5e6d6d2e8a9e3f879fba5cd0d8e175c774e5ceb Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 26 Nov 2016 16:02:44 +0900 Subject: [PATCH 11/16] Use default error process --- sanic/server.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 339b8132..b68524f8 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -13,7 +13,6 @@ except ImportError: from .log import log from .request import Request -from .response import HTTPResponse from .exceptions import RequestTimeout @@ -80,12 +79,8 @@ class HttpProtocol(asyncio.Protocol): self.loop.call_later(time_left, self.connection_timeout) else: self._request_handler_task.cancel() - try: - response = self.error_handler.response( - self.request, RequestTimeout('Request Timeout')) - except Exception as e: - response = HTTPResponse( - 'Request Timeout', RequestTimeout.status_code) + response = self.error_handler.response( + self.request, RequestTimeout('Request Timeout')) self.write_response(response) # -------------------------------------------- # From ee89b6ad03839bbe526f7e84958f9720e9a30e3f Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 26 Nov 2016 16:47:16 +0900 Subject: [PATCH 12/16] before process --- sanic/server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index b68524f8..dd582325 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -78,7 +78,8 @@ class HttpProtocol(asyncio.Protocol): self._timeout_handler = \ self.loop.call_later(time_left, self.connection_timeout) else: - self._request_handler_task.cancel() + if self._request_handler_task: + self._request_handler_task.cancel() response = self.error_handler.response( self.request, RequestTimeout('Request Timeout')) self.write_response(response) From 190b7a607610551d1071dae5ee682c45efb7c00a Mon Sep 17 00:00:00 2001 From: Derek Schuster Date: Mon, 28 Nov 2016 14:00:39 -0500 Subject: [PATCH 13/16] improving comments and examples --- sanic/request.py | 4 ++-- sanic/router.py | 12 +++++++++--- sanic/utils.py | 4 ++-- sanic/views.py | 8 +++++--- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 8023fd9c..bc7fcabb 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -67,7 +67,7 @@ class Request(dict): try: self.parsed_json = json_loads(self.body) except Exception: - log.exception("failed when parsing body as json") + log.exception("Failed when parsing body as json") return self.parsed_json @@ -89,7 +89,7 @@ class Request(dict): self.parsed_form, self.parsed_files = ( parse_multipart_form(self.body, boundary)) except Exception: - log.exception("failed when parsing form") + log.exception("Failed when parsing form") return self.parsed_form diff --git a/sanic/router.py b/sanic/router.py index 8392dcd8..0a1faec5 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -30,11 +30,17 @@ class Router: @sanic.route('/my/url/', methods=['GET', 'POST', ...]) def my_route(request, my_parameter): do stuff... + or + @sanic.route('/my/url/:int', methods['GET', 'POST', ...]) + def my_route_with_type(request, my_parameter): + do stuff... Parameters will be passed as keyword arguments to the request handling - function provided Parameters can also have a type by appending :type to - the . If no type is provided, a string is expected. A regular - expression can also be passed in as the type + function. Provided parameters can also have a type by appending :type to + the . Given parameter must be able to be type-casted to this. + If no type is provided, a string is expected. A regular expression can + also be passed in as the type. The argument given to the function will + always be a string, independent of the type. """ routes_static = None routes_dynamic = None diff --git a/sanic/utils.py b/sanic/utils.py index 5d896312..88444b3c 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -47,11 +47,11 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, return request, response except: raise ValueError( - "request and response object expected, got ({})".format( + "Request and response object expected, got ({})".format( results)) else: try: return results[0] except: raise ValueError( - "request object expected, got ({})".format(results)) + "Request object expected, got ({})".format(results)) diff --git a/sanic/views.py b/sanic/views.py index 980a5f74..440702bd 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -3,8 +3,9 @@ from .exceptions import InvalidUsage class HTTPMethodView: """ Simple class based implementation of view for the sanic. - You should implement methods(get, post, put, patch, delete) for the class + You should implement methods (get, post, put, patch, delete) for the class to every HTTP method you want to support. + For example: class DummyView(View): @@ -14,9 +15,10 @@ class HTTPMethodView: def put(self, request, *args, **kwargs): return text('I am put method') etc. - If someone try use not implemented method, there will be 405 response - If you need any url params just mention them in method definition like: + If someone tries to use a non-implemented method, there will be a 405 response. + + If you need any url params just mention them in method definition: class DummyView(View): def get(self, request, my_param_here, *args, **kwargs): From 209b7633025e63f1c6f163f3024139d50bd9dabd Mon Sep 17 00:00:00 2001 From: Derek Schuster Date: Mon, 28 Nov 2016 14:05:47 -0500 Subject: [PATCH 14/16] fix typo --- sanic/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/router.py b/sanic/router.py index 0a1faec5..4cc1f073 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -31,7 +31,7 @@ class Router: def my_route(request, my_parameter): do stuff... or - @sanic.route('/my/url/:int', methods['GET', 'POST', ...]) + @sanic.route('/my/url/:type', methods['GET', 'POST', ...]) def my_route_with_type(request, my_parameter): do stuff... From 70c56b7db33b558ff5fd5a95883fcb558b9621ba Mon Sep 17 00:00:00 2001 From: Derek Schuster Date: Mon, 28 Nov 2016 14:22:07 -0500 Subject: [PATCH 15/16] fixing line length --- sanic/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sanic/views.py b/sanic/views.py index 440702bd..9387bcf6 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -16,7 +16,8 @@ class HTTPMethodView: return text('I am put method') etc. - If someone tries to use a non-implemented method, there will be a 405 response. + If someone tries to use a non-implemented method, there will be a + 405 response. If you need any url params just mention them in method definition: class DummyView(View): From 39f3a63cede12ee40e17073a95578d37a730c158 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Tue, 29 Nov 2016 15:59:03 -0600 Subject: [PATCH 16/16] Increment version to 0.1.8 --- sanic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/__init__.py b/sanic/__init__.py index d8a9e56e..6e7f8d23 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1,6 +1,6 @@ from .sanic import Sanic from .blueprints import Blueprint -__version__ = '0.1.7' +__version__ = '0.1.8' __all__ = ['Sanic', 'Blueprint']