From f3fc958a0c8bea34df8b278804411dd4aa761ef0 Mon Sep 17 00:00:00 2001 From: Clenimar Filemon Date: Thu, 27 Oct 2016 11:09:36 -0300 Subject: [PATCH 001/134] Fix comments over-indentation --- sanic/server.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index e4bff6fc..ca5b2974 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -45,8 +45,7 @@ class HttpProtocol(asyncio.Protocol): self._total_request_size = 0 self._timeout_handler = None - # -------------------------------------------- # - + # -------------------------------------------- # # Connection # -------------------------------------------- # @@ -64,8 +63,7 @@ class HttpProtocol(asyncio.Protocol): def connection_timeout(self): self.bail_out("Request timed out, connection closed") - # -------------------------------------------- # - + # -------------------------------------------- # # Parsing # -------------------------------------------- # From bd28da0abc16bb37384f72d6b45ac8c4c34ce35c Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Fri, 28 Oct 2016 02:56:32 -0700 Subject: [PATCH 002/134] Keep-alive requests stay open if communicating --- sanic/server.py | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index e4bff6fc..94f59b37 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -1,4 +1,5 @@ import asyncio +from functools import partial from inspect import isawaitable from signal import SIGINT, SIGTERM @@ -16,7 +17,6 @@ from .request import Request class Signal: stopped = False - class HttpProtocol(asyncio.Protocol): __slots__ = ( # event loop, connection @@ -26,7 +26,7 @@ class HttpProtocol(asyncio.Protocol): # request config 'request_handler', 'request_timeout', 'request_max_size', # connection management - '_total_request_size', '_timeout_handler') + '_total_request_size', '_timeout_handler', '_last_communication_time') def __init__(self, *, loop, request_handler, signal=Signal(), connections={}, request_timeout=60, @@ -44,6 +44,7 @@ class HttpProtocol(asyncio.Protocol): self.request_max_size = request_max_size self._total_request_size = 0 self._timeout_handler = None + self._last_request_time = None # -------------------------------------------- # @@ -55,6 +56,7 @@ class HttpProtocol(asyncio.Protocol): self._timeout_handler = self.loop.call_later( self.request_timeout, self.connection_timeout) self.transport = transport + self._last_request_time = current_time def connection_lost(self, exc): del self.connections[self] @@ -62,7 +64,14 @@ class HttpProtocol(asyncio.Protocol): self.cleanup() def connection_timeout(self): - self.bail_out("Request timed out, connection closed") + # Check if + time_elapsed = current_time - self._last_request_time + if time_elapsed < self.request_timeout: + time_left = self.request_timeout - time_elapsed + self._timeout_handler = \ + self.loop.call_later(time_left, self.connection_timeout) + else: + self.bail_out("Request timed out, connection closed") # -------------------------------------------- # @@ -133,13 +142,15 @@ class HttpProtocol(asyncio.Protocol): if not keep_alive: self.transport.close() else: + # Record that we received data + self._last_request_time = current_time self.cleanup() except Exception as e: self.bail_out( "Writing request failed, connection closed {}".format(e)) def bail_out(self, message): - log.error(message) + log.debug(message) self.transport.close() def cleanup(self): @@ -159,6 +170,19 @@ class HttpProtocol(asyncio.Protocol): return True return False +# Keep check on the current time +current_time = None +def update_current_time(loop): + """ + Caches the current time, since it is needed + at the end of every keep-alive request to update the request timeout time + :param loop: + :return: + """ + global current_time + current_time = loop.time() + loop.call_later(0.5, partial(update_current_time, loop)) + def trigger_events(events, loop): """ @@ -173,7 +197,6 @@ def trigger_events(events, loop): if isawaitable(result): loop.run_until_complete(result) - def serve(host, port, request_handler, before_start=None, after_start=None, before_stop=None, after_stop=None, debug=False, request_timeout=60, sock=None, @@ -214,6 +237,10 @@ def serve(host, port, request_handler, before_start=None, after_start=None, request_max_size=request_max_size, ), host, port, reuse_port=reuse_port, sock=sock) + # Instead of pulling time at the end of every request, + # pull it once per minute + loop.call_soon(partial(update_current_time, loop)) + try: http_server = loop.run_until_complete(server_coroutine) except Exception: From c44b5551bcc0b7726fe695dbcd03ece5123a4905 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Fri, 28 Oct 2016 03:13:03 -0700 Subject: [PATCH 003/134] time.time faster than loop.time? --- sanic/server.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 94f59b37..5440b706 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -2,6 +2,7 @@ import asyncio from functools import partial from inspect import isawaitable from signal import SIGINT, SIGTERM +from time import time import httptools @@ -180,8 +181,8 @@ def update_current_time(loop): :return: """ global current_time - current_time = loop.time() - loop.call_later(0.5, partial(update_current_time, loop)) + current_time = time() + loop.call_later(1, partial(update_current_time, loop)) def trigger_events(events, loop): From 707c55fbe71a5e1fc8aad7b1ff41cd0733a8f909 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Fri, 28 Oct 2016 03:35:30 -0700 Subject: [PATCH 004/134] Fix flake8 --- sanic/server.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 5440b706..9c2ea749 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -18,6 +18,10 @@ from .request import Request class Signal: stopped = False + +current_time = None + + class HttpProtocol(asyncio.Protocol): __slots__ = ( # event loop, connection @@ -171,8 +175,7 @@ class HttpProtocol(asyncio.Protocol): return True return False -# Keep check on the current time -current_time = None + def update_current_time(loop): """ Caches the current time, since it is needed @@ -198,6 +201,7 @@ def trigger_events(events, loop): if isawaitable(result): loop.run_until_complete(result) + def serve(host, port, request_handler, before_start=None, after_start=None, before_stop=None, after_stop=None, debug=False, request_timeout=60, sock=None, From 96fcd8443f7994f73d6e82525d14009995a18f6a Mon Sep 17 00:00:00 2001 From: Ryan Kung Date: Tue, 1 Nov 2016 14:35:06 +0800 Subject: [PATCH 005/134] Update README.md via flake8 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a02dc703..65c3eda7 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ app = Sanic(__name__) @app.route("/") async def test(request): - return json({ "hello": "world" }) + return json({"hello": "world"}) app.run(host="0.0.0.0", port=8000) ``` From 3cd3b2d9b7c463b8490d9409674fc4fb63b347a9 Mon Sep 17 00:00:00 2001 From: imbolc Date: Thu, 3 Nov 2016 12:34:55 +0700 Subject: [PATCH 006/134] Fix upload without content-type --- sanic/request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/request.py b/sanic/request.py index 2687d86b..c2ab7260 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -71,7 +71,7 @@ class Request: self.parsed_form = {} self.parsed_files = {} content_type, parameters = parse_header( - self.headers.get('Content-Type')) + self.headers.get('Content-Type', '')) try: is_url_encoded = ( content_type == 'application/x-www-form-urlencoded') From df2f91b82f9a2b666e0da0c9aa179c4de01b4a72 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Thu, 3 Nov 2016 09:35:14 -0500 Subject: [PATCH 007/134] Add aiofiles to requirements.txt --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2e0feec8..cef8660e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ httptools ujson -uvloop \ No newline at end of file +uvloop +aiofiles From 3a2eeb97095330a95be790c048d982a25d6ec70a Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 5 Nov 2016 13:12:55 -0500 Subject: [PATCH 008/134] Fix value error for query string test --- tests/test_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_requests.py b/tests/test_requests.py index 290c9b99..756113b2 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -56,7 +56,7 @@ def test_query_string(): async def handler(request): return text('OK') - request, response = sanic_endpoint_test(app, params=[("test1", 1), ("test2", "false"), ("test2", "true")]) + request, response = sanic_endpoint_test(app, params=[("test1", "1"), ("test2", "false"), ("test2", "true")]) assert request.args.get('test1') == '1' assert request.args.get('test2') == 'false' From ce8742c60562eb1f718704b317101542505471ca Mon Sep 17 00:00:00 2001 From: Manuel Miranda Date: Sun, 6 Nov 2016 16:26:15 +0100 Subject: [PATCH 009/134] Caching example using aiocache (#140) * Keep-alive requests stay open if communicating * time.time faster than loop.time? * Fix flake8 * Add aiofiles to requirements.txt * Caching example using aiocache * Caching example using aiocache * Added aiocache to requirements --- examples/cache_example.py | 38 ++++++++++++++++++++++++++++++++++++++ requirements-dev.txt | 3 ++- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 examples/cache_example.py diff --git a/examples/cache_example.py b/examples/cache_example.py new file mode 100644 index 00000000..44cc7140 --- /dev/null +++ b/examples/cache_example.py @@ -0,0 +1,38 @@ +""" +Example of caching using aiocache package. To run it you will need a Redis +instance running in localhost:6379. + +Running this example you will see that the first call lasts 3 seconds and +the rest are instant because the value is retrieved from the Redis. + +If you want more info about the package check +https://github.com/argaen/aiocache +""" + +import asyncio +import aiocache + +from sanic import Sanic +from sanic.response import json +from sanic.log import log +from aiocache import RedisCache, cached +from aiocache.serializers import JsonSerializer + +app = Sanic(__name__) +aiocache.set_defaults(cache=RedisCache) + + +@cached(key="my_custom_key", serializer=JsonSerializer()) +async def expensive_call(): + log.info("Expensive has been called") + await asyncio.sleep(3) + return {"test": True} + + +@app.route("/") +async def test(request): + log.info("Received GET /") + return json(await expensive_call()) + + +app.run(host="0.0.0.0", port=8000, loop=asyncio.get_event_loop()) diff --git a/requirements-dev.txt b/requirements-dev.txt index 9593b0cf..00feb17d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,6 +2,7 @@ httptools ujson uvloop aiohttp +aiocache pytest coverage tox @@ -9,4 +10,4 @@ gunicorn bottle kyoukai falcon -tornado \ No newline at end of file +tornado From 1b65b2e0c658dc0e87a5a93f3e3665782b01edef Mon Sep 17 00:00:00 2001 From: Pahaz Blinov Date: Sun, 6 Nov 2016 21:08:55 +0500 Subject: [PATCH 010/134] fix(blueprints): @middleware IndexError (#139) --- sanic/blueprints.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sanic/blueprints.py b/sanic/blueprints.py index c9c54b62..bfef8557 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -109,8 +109,9 @@ class Blueprint: # Detect which way this was called, @middleware or @middleware('AT') if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): + middleware = args[0] args = [] - return register_middleware(args[0]) + return register_middleware(middleware) else: return register_middleware From 5efe51b66129d5bc902f0a70f0290b94fbd17c84 Mon Sep 17 00:00:00 2001 From: Pahaz Blinov Date: Tue, 8 Nov 2016 02:27:50 +0500 Subject: [PATCH 011/134] fix(request.py): problem in case of request without content-type header (#142) * fix(request.py): exception if access request.form on GET request * fix(request): just make a unification (parsed_form and parsed_files) + RFC fixes parsed_form and parsed_files must be a RequestParameters type in all cases! --- sanic/request.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index c2ab7260..109b1483 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -8,6 +8,12 @@ from ujson import loads as json_loads from .log import log +DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" +# HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1 +# > If the media type remains unknown, the recipient SHOULD treat it +# > as type "application/octet-stream" + + class RequestParameters(dict): """ Hosts a dict with lists as values where get returns the first @@ -68,14 +74,13 @@ class Request: @property def form(self): if self.parsed_form is None: - self.parsed_form = {} - self.parsed_files = {} - content_type, parameters = parse_header( - self.headers.get('Content-Type', '')) + self.parsed_form = RequestParameters() + self.parsed_files = RequestParameters() + content_type = self.headers.get( + 'Content-Type', DEFAULT_HTTP_CONTENT_TYPE) + content_type, parameters = parse_header(content_type) try: - is_url_encoded = ( - content_type == 'application/x-www-form-urlencoded') - if content_type is None or is_url_encoded: + if content_type == 'application/x-www-form-urlencoded': self.parsed_form = RequestParameters( parse_qs(self.body.decode('utf-8'))) elif content_type == 'multipart/form-data': @@ -86,7 +91,6 @@ class Request: except Exception as e: log.exception(e) pass - return self.parsed_form @property From 0e9819fba168ee2aa6b32827db751bb4c0a70dab Mon Sep 17 00:00:00 2001 From: Pahaz Blinov Date: Wed, 9 Nov 2016 00:36:37 +0500 Subject: [PATCH 012/134] fix(request): parse_multipart_form should return RequestParameters I have this code: ``` form = FileForm(request.files) ``` and it raise error because the `request.files` is `dict` but `RequestParameters` is expected =/ --- sanic/request.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 109b1483..a843b73b 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -134,8 +134,8 @@ def parse_multipart_form(body, boundary): :param boundary: Bytes multipart boundary :return: fields (dict), files (dict) """ - files = {} - fields = {} + files = RequestParameters() + fields = RequestParameters() form_parts = body.split(boundary) for form_part in form_parts[1:-1]: @@ -166,9 +166,16 @@ def parse_multipart_form(body, boundary): post_data = form_part[line_index:-4] if file_name or file_type: - files[field_name] = File( - type=file_type, name=file_name, body=post_data) + file = File(type=file_type, name=file_name, body=post_data) + if field_name in files: + files[field_name].append(file) + else: + files[field_name] = [file] else: - fields[field_name] = post_data.decode('utf-8') + value = post_data.decode('utf-8') + if field_name in fields: + fields[field_name].append(value) + else: + fields[field_name] = [value] return fields, files From 0d9fb2f9279e3303f51a0b18204a88d6cfbb1a0e Mon Sep 17 00:00:00 2001 From: Pahaz Blinov Date: Wed, 9 Nov 2016 18:04:15 +0500 Subject: [PATCH 013/134] docs(request): return value docstring --- sanic/request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/request.py b/sanic/request.py index a843b73b..7373a104 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -132,7 +132,7 @@ def parse_multipart_form(body, boundary): Parses a request body and returns fields and files :param body: Bytes request body :param boundary: Bytes multipart boundary - :return: fields (dict), files (dict) + :return: fields (RequestParameters), files (RequestParameters) """ files = RequestParameters() fields = RequestParameters() From be5588d5d80958778166e3cb321c875f260b0a17 Mon Sep 17 00:00:00 2001 From: Paul Jongsma Date: Thu, 10 Nov 2016 12:53:00 +0100 Subject: [PATCH 014/134] Add the client address to the request header --- sanic/server.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sanic/server.py b/sanic/server.py index 70dca448..729e1044 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -114,6 +114,11 @@ class HttpProtocol(asyncio.Protocol): self.headers.append((name.decode(), value.decode('utf-8'))) def on_headers_complete(self): + + ra = self.transport.get_extra_info('peername') + if ra: + self.headers.append(('Remote-Addr','%s:%s' % ra)) + self.request = Request( url_bytes=self.url, headers=dict(self.headers), From b92e46df4018aa9288e1918eca2f65e2820b61c7 Mon Sep 17 00:00:00 2001 From: Paul Jongsma Date: Thu, 10 Nov 2016 13:06:27 +0100 Subject: [PATCH 015/134] fix whitespace --- sanic/server.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 729e1044..ec4e5780 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -114,11 +114,10 @@ class HttpProtocol(asyncio.Protocol): self.headers.append((name.decode(), value.decode('utf-8'))) def on_headers_complete(self): - ra = self.transport.get_extra_info('peername') if ra: self.headers.append(('Remote-Addr','%s:%s' % ra)) - + self.request = Request( url_bytes=self.url, headers=dict(self.headers), From 8ebc92c236652f357394e618640505a11ba49283 Mon Sep 17 00:00:00 2001 From: Paul Jongsma Date: Thu, 10 Nov 2016 13:09:37 +0100 Subject: [PATCH 016/134] pass flake8 tests --- sanic/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index ec4e5780..e802b5df 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -116,7 +116,7 @@ class HttpProtocol(asyncio.Protocol): def on_headers_complete(self): ra = self.transport.get_extra_info('peername') if ra: - self.headers.append(('Remote-Addr','%s:%s' % ra)) + self.headers.append(('Remote-Addr', '%s:%s' % ra)) self.request = Request( url_bytes=self.url, From 28ce2447ef9c4bd75ad6e21bc9f3a660714a3b02 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Thu, 10 Nov 2016 15:28:16 -0600 Subject: [PATCH 017/134] Update variable name Give `ra` a more explicit name --- sanic/server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index e802b5df..0fd85440 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -114,9 +114,9 @@ class HttpProtocol(asyncio.Protocol): self.headers.append((name.decode(), value.decode('utf-8'))) def on_headers_complete(self): - ra = self.transport.get_extra_info('peername') - if ra: - self.headers.append(('Remote-Addr', '%s:%s' % ra)) + remote_addr = self.transport.get_extra_info('peername') + if remote_addr: + self.headers.append(('Remote-Addr', '%s:%s' % remote_addr)) self.request = Request( url_bytes=self.url, From 695f8733bbb9c35f82ec61f4c977e4828128cc19 Mon Sep 17 00:00:00 2001 From: The Gitter Badger Date: Fri, 11 Nov 2016 04:11:07 +0000 Subject: [PATCH 018/134] Add Gitter badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 65c3eda7..cc309c8c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Sanic +[![Join the chat at https://gitter.im/sanic-python/Lobby](https://badges.gitter.im/sanic-python/Lobby.svg)](https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + [![Build Status](https://travis-ci.org/channelcat/sanic.svg?branch=master)](https://travis-ci.org/channelcat/sanic) [![PyPI](https://img.shields.io/pypi/v/sanic.svg)](https://pypi.python.org/pypi/sanic/) [![PyPI](https://img.shields.io/pypi/pyversions/sanic.svg)](https://pypi.python.org/pypi/sanic/) From 0822674f70b05d42b87dfd82769cbc8222756468 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Fri, 11 Nov 2016 22:36:49 +0200 Subject: [PATCH 019/134] aiohttp is slightly faster actually Disabling access log increases RPS a lot --- tests/performance/aiohttp/simple_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/performance/aiohttp/simple_server.py b/tests/performance/aiohttp/simple_server.py index 8cb97b33..7c61f723 100644 --- a/tests/performance/aiohttp/simple_server.py +++ b/tests/performance/aiohttp/simple_server.py @@ -15,4 +15,4 @@ async def handle(request): app = web.Application(loop=loop) app.router.add_route('GET', '/', handle) -web.run_app(app, port=sys.argv[1]) +web.run_app(app, port=sys.argv[1], access_log=None) From edb25f799d2e85f3d715582fd2005cc80c8a6dd5 Mon Sep 17 00:00:00 2001 From: Manuel Miranda Date: Mon, 14 Nov 2016 00:11:31 +0100 Subject: [PATCH 020/134] Caching example (#150) * Caching example using aiocache * Caching example using aiocache * Added aiocache to requirements * Fixed example with newest aiocache --- examples/cache_example.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/cache_example.py b/examples/cache_example.py index 44cc7140..60823366 100644 --- a/examples/cache_example.py +++ b/examples/cache_example.py @@ -15,11 +15,14 @@ import aiocache from sanic import Sanic from sanic.response import json from sanic.log import log -from aiocache import RedisCache, cached +from aiocache import cached from aiocache.serializers import JsonSerializer app = Sanic(__name__) -aiocache.set_defaults(cache=RedisCache) + +aiocache.settings.set_defaults( + cache="aiocache.RedisCache" +) @cached(key="my_custom_key", serializer=JsonSerializer()) From 9e0747db15282f6f8f8474f32e6b9ff0f1bf9174 Mon Sep 17 00:00:00 2001 From: Jack Fischer Date: Tue, 15 Nov 2016 19:37:40 -0500 Subject: [PATCH 021/134] Example for using error_handler --- examples/exception_monitoring.py | 59 ++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 examples/exception_monitoring.py diff --git a/examples/exception_monitoring.py b/examples/exception_monitoring.py new file mode 100644 index 00000000..51f8bfba --- /dev/null +++ b/examples/exception_monitoring.py @@ -0,0 +1,59 @@ +""" +Example intercepting uncaught exceptions using Sanic's error handler framework. + +This may be useful for developers wishing to use Sentry, Airbrake, etc. +or a custom system to log and monitor unexpected errors in production. + +First we create our own class inheriting from Handler in sanic.exceptions, +and pass in an instance of it when we create our Sanic instance. Inside this +class' default handler, we can do anything including sending exceptions to +an external service. +""" + + + +""" +Imports and code relevant for our CustomHandler class +(Ordinarily this would be in a separate file) +""" +from sanic.response import text +from sanic.exceptions import Handler, SanicException + +class CustomHandler(Handler): + def default(self, request, exception): + # Here, we have access to the exception object + # and can do anything with it (log, send to external service, etc) + + # Some exceptions are trivial and built into Sanic (404s, etc) + if not issubclass(type(exception), SanicException): + print(exception) + + # Then, we must finish handling the exception by + # returning our response to the client + return text("An error occured", status=500) + + + + +""" +This is an ordinary Sanic server, with the exception that we set the +server's error_handler to an instance of our CustomHandler +""" + +from sanic import Sanic +from sanic.response import json + +app = Sanic(__name__) + +handler = CustomHandler(sanic=app) +app.error_handler = handler + +@app.route("/") +async def test(request): + # Here, something occurs which causes an unexpected exception + # This exception will flow to our custom handler. + x = 1 / 0 + return json({"test": True}) + + +app.run(host="0.0.0.0", port=8000) From d9f6846c7641e6ae9c4ea4c688a4d24f56e83a8b Mon Sep 17 00:00:00 2001 From: Jack Fischer Date: Wed, 16 Nov 2016 07:55:54 -0500 Subject: [PATCH 022/134] improved default handling --- examples/exception_monitoring.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/exception_monitoring.py b/examples/exception_monitoring.py index 51f8bfba..34b46a14 100644 --- a/examples/exception_monitoring.py +++ b/examples/exception_monitoring.py @@ -28,9 +28,10 @@ class CustomHandler(Handler): if not issubclass(type(exception), SanicException): print(exception) - # Then, we must finish handling the exception by - # returning our response to the client - return text("An error occured", status=500) + # Then, we must finish handling the exception by returning + # our response to the client + # For this we can just call the super class' default handler + return super.default(self, request, exception) @@ -56,4 +57,4 @@ async def test(request): return json({"test": True}) -app.run(host="0.0.0.0", port=8000) +app.run(host="0.0.0.0", port=8000, debug=True) From edb12da154f30b208571f43aac018bd56e587b19 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Wed, 16 Nov 2016 12:55:13 -0600 Subject: [PATCH 023/134] Fix the flake8 error caused by new flake8 version --- sanic/cookies.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sanic/cookies.py b/sanic/cookies.py index 622a5a08..b7669e76 100644 --- a/sanic/cookies.py +++ b/sanic/cookies.py @@ -30,6 +30,7 @@ def _quote(str): else: return '"' + str.translate(_Translator) + '"' + _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch # ------------------------------------------------------------ # From f16ea20de5fa844c53639f91cf521d2993806fb8 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Fri, 18 Nov 2016 17:06:16 -0800 Subject: [PATCH 024/134] provide default app name --- sanic/sanic.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index edb3a973..cbc12278 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -1,7 +1,7 @@ from asyncio import get_event_loop from collections import deque from functools import partial -from inspect import isawaitable +from inspect import isawaitable, stack, getmodulename from multiprocessing import Process, Event from signal import signal, SIGTERM, SIGINT from time import sleep @@ -18,7 +18,10 @@ from .exceptions import ServerError class Sanic: - def __init__(self, name, router=None, error_handler=None): + def __init__(self, name=None, router=None, error_handler=None): + if name is None: + frame_records = stack()[1] + name = getmodulename(frame_records[1]) self.name = name self.router = router or Router() self.error_handler = error_handler or Handler(self) From 8be4dc8fb52f27a4a51cc5d31a1aa6a767450450 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Fri, 18 Nov 2016 17:22:16 -0800 Subject: [PATCH 025/134] update readme example to use default --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc309c8c..4b6d87de 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ All tests were run on an AWS medium instance running ubuntu, using 1 process. E from sanic import Sanic from sanic.response import json -app = Sanic(__name__) +app = Sanic() @app.route("/") async def test(request): From 9eb4cecbc1674321b3e37a7ae08afb996d5bd890 Mon Sep 17 00:00:00 2001 From: jiajunhuang Date: Sat, 19 Nov 2016 15:16:20 +0800 Subject: [PATCH 026/134] fix the way using logging.exception --- sanic/request.py | 8 ++++---- sanic/sanic.py | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 7373a104..82fc64db 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -67,7 +67,7 @@ class Request: try: self.parsed_json = json_loads(self.body) except Exception: - pass + log.exception("failed when parsing body as json") return self.parsed_json @@ -88,9 +88,9 @@ class Request: boundary = parameters['boundary'].encode('utf-8') self.parsed_form, self.parsed_files = ( parse_multipart_form(self.body, boundary)) - except Exception as e: - log.exception(e) - pass + except Exception: + log.exception("failed when parsing form") + return self.parsed_form @property diff --git a/sanic/sanic.py b/sanic/sanic.py index cbc12278..af284c00 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -295,8 +295,7 @@ class Sanic: except Exception as e: log.exception( - 'Experienced exception while trying to serve: {}'.format(e)) - pass + 'Experienced exception while trying to serve') log.info("Server Stopped") From 635921adc71c8d27bc315f3b18ad927b6e14a5a5 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 19 Nov 2016 16:03:09 -0800 Subject: [PATCH 027/134] Update headers to use CIMultiDict instead of dict --- sanic/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 0fd85440..0afeca23 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -3,7 +3,7 @@ from functools import partial from inspect import isawaitable from signal import SIGINT, SIGTERM from time import time - +from aiohttp import CIMultiDict import httptools try: @@ -120,7 +120,7 @@ class HttpProtocol(asyncio.Protocol): self.request = Request( url_bytes=self.url, - headers=dict(self.headers), + headers=CIMultiDict(self.headers), version=self.parser.get_http_version(), method=self.parser.get_method().decode() ) From a97e554f8f4bd2443d6a9d68c60351a8f86c10bc Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sat, 19 Nov 2016 17:48:28 -0800 Subject: [PATCH 028/134] Added shared request data --- sanic/request.py | 2 +- tests/test_request_data.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 tests/test_request_data.py diff --git a/sanic/request.py b/sanic/request.py index c2ab7260..ff6e8b51 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -26,7 +26,7 @@ class RequestParameters(dict): return self.super.get(name, default) -class Request: +class Request(dict): """ Properties of an HTTP request such as URL, headers, etc. """ diff --git a/tests/test_request_data.py b/tests/test_request_data.py new file mode 100644 index 00000000..161a7836 --- /dev/null +++ b/tests/test_request_data.py @@ -0,0 +1,24 @@ +from sanic import Sanic +from sanic.response import json +from sanic.utils import sanic_endpoint_test +from ujson import loads + + +def test_storage(): + app = Sanic('test_text') + + @app.middleware('request') + def store(request): + request['a'] = 'test' + request['b'] = 'zest' + del request['b'] + + @app.route('/') + def handler(request): + return json({ 'a': request.get('a'), 'b': request.get('b') }) + + request, response = sanic_endpoint_test(app) + + response_json = loads(response.text) + assert response_json['a'] == 'test' + assert response_json.get('b') is None \ No newline at end of file From 3ce6434532fcf7994a50f0d780320cb3b1e6e586 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sat, 19 Nov 2016 18:04:35 -0800 Subject: [PATCH 029/134] Fix flake8 --- sanic/cookies.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sanic/cookies.py b/sanic/cookies.py index 622a5a08..b7669e76 100644 --- a/sanic/cookies.py +++ b/sanic/cookies.py @@ -30,6 +30,7 @@ def _quote(str): else: return '"' + str.translate(_Translator) + '"' + _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch # ------------------------------------------------------------ # From 01681599ff63384e8f87680df641c178b3c2484f Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sat, 19 Nov 2016 18:13:02 -0800 Subject: [PATCH 030/134] Fixed new test error with aiohttp --- tests/test_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_requests.py b/tests/test_requests.py index 290c9b99..0098797d 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -56,7 +56,7 @@ def test_query_string(): async def handler(request): return text('OK') - request, response = sanic_endpoint_test(app, params=[("test1", 1), ("test2", "false"), ("test2", "true")]) + request, response = sanic_endpoint_test(app, params={"test1":1, "test2":"false"}) assert request.args.get('test1') == '1' assert request.args.get('test2') == 'false' From 922c96e3c1ca28572b631b63145f717774f5efd2 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sat, 19 Nov 2016 18:26:03 -0800 Subject: [PATCH 031/134] Updated test terminology --- tests/test_request_data.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_request_data.py b/tests/test_request_data.py index 161a7836..fca67ba9 100644 --- a/tests/test_request_data.py +++ b/tests/test_request_data.py @@ -9,16 +9,16 @@ def test_storage(): @app.middleware('request') def store(request): - request['a'] = 'test' - request['b'] = 'zest' - del request['b'] + request['user'] = 'sanic' + request['sidekick'] = 'tails' + del request['sidekick'] @app.route('/') def handler(request): - return json({ 'a': request.get('a'), 'b': request.get('b') }) + return json({ 'user': request.get('user'), 'sidekick': request.get('sidekick') }) request, response = sanic_endpoint_test(app) response_json = loads(response.text) - assert response_json['a'] == 'test' - assert response_json.get('b') is None \ No newline at end of file + assert response_json['user'] == 'sanic' + assert response_json.get('sidekick') is None \ No newline at end of file From d02fffb6b84d4d9e7e728c38c6115ee3b417d662 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sat, 19 Nov 2016 18:41:40 -0800 Subject: [PATCH 032/134] Fixing import of CIMultiDict --- requirements.txt | 1 + sanic/server.py | 2 +- setup.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cef8660e..3acfbb1f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ httptools ujson uvloop aiofiles +multidict diff --git a/sanic/server.py b/sanic/server.py index 0afeca23..9081b729 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -1,9 +1,9 @@ import asyncio from functools import partial from inspect import isawaitable +from multidict import CIMultiDict from signal import SIGINT, SIGTERM from time import time -from aiohttp import CIMultiDict import httptools try: diff --git a/setup.py b/setup.py index 60606ad4..e6e9b4cc 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ setup( 'httptools>=0.0.9', 'ujson>=1.35', 'aiofiles>=0.3.0', + 'multidict>=2.0', ], classifiers=[ 'Development Status :: 2 - Pre-Alpha', From f7f578ed4494298170b885c9631274432dcc5ed5 Mon Sep 17 00:00:00 2001 From: abhishek7 Date: Sun, 20 Nov 2016 21:37:01 -0500 Subject: [PATCH 033/134] Fixed Exception error log on line 157 of server.py --- sanic/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index 9081b729..b6233031 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -154,7 +154,7 @@ class HttpProtocol(asyncio.Protocol): self.cleanup() except Exception as e: self.bail_out( - "Writing request failed, connection closed {}".format(e)) + "Writing response failed, connection closed {}".format(e)) def bail_out(self, message): log.debug(message) From aa0f15fbb2de8bd644eaff869e6ee94880d19802 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Wed, 23 Nov 2016 11:03:00 -0600 Subject: [PATCH 034/134] Adding a new line --- tests/test_request_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_request_data.py b/tests/test_request_data.py index fca67ba9..098878e7 100644 --- a/tests/test_request_data.py +++ b/tests/test_request_data.py @@ -21,4 +21,4 @@ def test_storage(): response_json = loads(response.text) assert response_json['user'] == 'sanic' - assert response_json.get('sidekick') is None \ No newline at end of file + assert response_json.get('sidekick') is None From 9f2d73e2f152456df27941efc3d5fe2695e0af6b Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Fri, 25 Nov 2016 09:10:25 +0200 Subject: [PATCH 035/134] 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 036/134] 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 037/134] 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 13808bf282493fccf81458da129514d5e28e88ac Mon Sep 17 00:00:00 2001 From: John Piasetzki Date: Fri, 25 Nov 2016 14:53:18 -0500 Subject: [PATCH 038/134] Convert server lambda to partial Partials are faster then lambdas for repeated calls. --- sanic/server.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index b6233031..edc96968 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -235,14 +235,23 @@ def serve(host, port, request_handler, before_start=None, after_start=None, connections = {} signal = Signal() - server_coroutine = loop.create_server(lambda: HttpProtocol( + server = partial( + HttpProtocol, loop=loop, connections=connections, signal=signal, request_handler=request_handler, request_timeout=request_timeout, request_max_size=request_max_size, - ), host, port, reuse_port=reuse_port, sock=sock) + ) + + server_coroutine = loop.create_server( + server, + host, + port, + reuse_port=reuse_port, + sock=sock + ) # Instead of pulling time at the end of every request, # pull it once per minute From 47927608b2c02cab80270388c0ecff4f0ccb493c Mon Sep 17 00:00:00 2001 From: John Piasetzki Date: Fri, 25 Nov 2016 15:05:17 -0500 Subject: [PATCH 039/134] Convert connections dict to set Connections don't need to be a dict since the value is never used --- sanic/server.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index b6233031..dd3c86e0 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -34,7 +34,7 @@ class HttpProtocol(asyncio.Protocol): '_total_request_size', '_timeout_handler', '_last_communication_time') def __init__(self, *, loop, request_handler, signal=Signal(), - connections={}, request_timeout=60, + connections=set(), request_timeout=60, request_max_size=None): self.loop = loop self.transport = None @@ -56,14 +56,14 @@ class HttpProtocol(asyncio.Protocol): # -------------------------------------------- # def connection_made(self, transport): - self.connections[self] = True + self.connections.add(self) self._timeout_handler = self.loop.call_later( self.request_timeout, self.connection_timeout) self.transport = transport self._last_request_time = current_time def connection_lost(self, exc): - del self.connections[self] + self.connections.discard(self) self._timeout_handler.cancel() self.cleanup() @@ -233,7 +233,7 @@ def serve(host, port, request_handler, before_start=None, after_start=None, trigger_events(before_start, loop) - connections = {} + connections = set() signal = Signal() server_coroutine = loop.create_server(lambda: HttpProtocol( loop=loop, @@ -274,7 +274,7 @@ def serve(host, port, request_handler, before_start=None, after_start=None, # Complete all tasks on the loop signal.stopped = True - for connection in connections.keys(): + for connection in connections: connection.close_if_idle() while connections: From 0ca5c4eeff5e4eab6f986703acc7f3b7f3ef18c2 Mon Sep 17 00:00:00 2001 From: John Piasetzki Date: Fri, 25 Nov 2016 14:33:17 -0500 Subject: [PATCH 040/134] 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 041/134] 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 042/134] 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 043/134] 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 044/134] 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 045/134] 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 046/134] 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 047/134] 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 048/134] 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 d86ac5e3e03f3a1469e9f4e1bce9af1919fbaa54 Mon Sep 17 00:00:00 2001 From: Jack Fischer Date: Sat, 26 Nov 2016 11:20:29 -0500 Subject: [PATCH 049/134] fix for cookie header capitalization bug --- sanic/request.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sanic/request.py b/sanic/request.py index 8023fd9c..676eaa51 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -114,6 +114,8 @@ class Request(dict): @property def cookies(self): if self._cookies is None: + if 'cookie' in self.headers: #HTTP2 cookie header + self.headers['Cookie'] = self.headers.pop('cookie') if 'Cookie' in self.headers: cookies = SimpleCookie() cookies.load(self.headers['Cookie']) From 0c215685f2e08ef4d5ff3b16d849194d81557133 Mon Sep 17 00:00:00 2001 From: Jack Fischer Date: Sun, 27 Nov 2016 08:30:46 -0500 Subject: [PATCH 050/134] refactoring cookies --- sanic/request.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 676eaa51..e5da4ce3 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -114,11 +114,10 @@ class Request(dict): @property def cookies(self): if self._cookies is None: - if 'cookie' in self.headers: #HTTP2 cookie header - self.headers['Cookie'] = self.headers.pop('cookie') - if 'Cookie' in self.headers: + cookie = self.headers.get('Cookie') or self.headers.get('cookie') + if cookie is not None: cookies = SimpleCookie() - cookies.load(self.headers['Cookie']) + cookies.load(cookie) self._cookies = {name: cookie.value for name, cookie in cookies.items()} else: From 190b7a607610551d1071dae5ee682c45efb7c00a Mon Sep 17 00:00:00 2001 From: Derek Schuster Date: Mon, 28 Nov 2016 14:00:39 -0500 Subject: [PATCH 051/134] 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 052/134] 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 053/134] 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 054/134] 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'] From 9b466db5c9d1d717cfb136cacb741f59333445c3 Mon Sep 17 00:00:00 2001 From: Jack Fischer Date: Sat, 3 Dec 2016 15:19:24 -0500 Subject: [PATCH 055/134] test for http2 lowercase header cookies --- tests/test_cookies.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_cookies.py b/tests/test_cookies.py index 5b27c2e7..cf6a4259 100644 --- a/tests/test_cookies.py +++ b/tests/test_cookies.py @@ -25,6 +25,19 @@ def test_cookies(): assert response.text == 'Cookies are: working!' assert response_cookies['right_back'].value == 'at you' +def test_http2_cookies(): + app = Sanic('test_http2_cookies') + + @app.route('/') + async def handler(request): + response = text('Cookies are: {}'.format(request.cookies['test'])) + return response + + headers = {'cookie': 'test=working!'} + request, response = sanic_endpoint_test(app, headers=headers) + + assert response.text == 'Cookies are: working!' + def test_cookie_options(): app = Sanic('test_text') From 662e0c9965b71a1b18bf689b3ca972ce36ec2ec2 Mon Sep 17 00:00:00 2001 From: 38elements Date: Sun, 4 Dec 2016 10:50:32 +0900 Subject: [PATCH 056/134] Change Payload Too Large process When Payload Too Large occurs, it uses error handler. --- sanic/exceptions.py | 4 +++ sanic/server.py | 26 ++++++++++------ tests/test_payload_too_large.py | 54 +++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 tests/test_payload_too_large.py diff --git a/sanic/exceptions.py b/sanic/exceptions.py index bc052fbd..369a87a2 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -34,6 +34,10 @@ class RequestTimeout(SanicException): status_code = 408 +class PayloadTooLarge(SanicException): + status_code = 413 + + class Handler: handlers = None diff --git a/sanic/server.py b/sanic/server.py index a3074ecf..534436fa 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -14,7 +14,7 @@ except ImportError: from .log import log from .request import Request -from .exceptions import RequestTimeout +from .exceptions import RequestTimeout, PayloadTooLarge class Signal: @@ -81,9 +81,8 @@ class HttpProtocol(asyncio.Protocol): else: if self._request_handler_task: self._request_handler_task.cancel() - response = self.error_handler.response( - self.request, RequestTimeout('Request Timeout')) - self.write_response(response) + exception = RequestTimeout('Request Timeout') + self.write_error(exception) # -------------------------------------------- # # Parsing @@ -94,9 +93,8 @@ class HttpProtocol(asyncio.Protocol): # memory limits self._total_request_size += len(data) if self._total_request_size > self.request_max_size: - return self.bail_out( - "Request too large ({}), connection closed".format( - self._total_request_size)) + exception = PayloadTooLarge('Payload Too Large') + self.write_error(exception) # Create parser if this is the first time we're receiving data if self.parser is None: @@ -116,8 +114,8 @@ class HttpProtocol(asyncio.Protocol): def on_header(self, name, value): if name == b'Content-Length' and int(value) > self.request_max_size: - return self.bail_out( - "Request body too large ({}), connection closed".format(value)) + exception = PayloadTooLarge('Payload Too Large') + self.write_error(exception) self.headers.append((name.decode(), value.decode('utf-8'))) @@ -164,6 +162,16 @@ class HttpProtocol(asyncio.Protocol): self.bail_out( "Writing response failed, connection closed {}".format(e)) + def write_error(self, exception): + try: + response = self.error_handler.response(self.request, exception) + version = self.request.version if self.request else '1.1' + self.transport.write(response.output(version)) + self.transport.close() + except Exception as e: + self.bail_out( + "Writing error failed, connection closed {}".format(e)) + def bail_out(self, message): log.debug(message) self.transport.close() diff --git a/tests/test_payload_too_large.py b/tests/test_payload_too_large.py new file mode 100644 index 00000000..e8eec09e --- /dev/null +++ b/tests/test_payload_too_large.py @@ -0,0 +1,54 @@ +from sanic import Sanic +from sanic.response import text +from sanic.exceptions import PayloadTooLarge +from sanic.utils import sanic_endpoint_test + +data_received_app = Sanic('data_received') +data_received_app.config.REQUEST_MAX_SIZE = 1 +data_received_default_app = Sanic('data_received_default') +data_received_default_app.config.REQUEST_MAX_SIZE = 1 +on_header_default_app = Sanic('on_header') +on_header_default_app.config.REQUEST_MAX_SIZE = 500 + + +@data_received_app.route('/1') +async def handler1(request): + return text('OK') + + +@data_received_app.exception(PayloadTooLarge) +def handler_exception(request, exception): + return text('Payload Too Large from error_handler.', 413) + + +def test_payload_too_large_from_error_handler(): + response = sanic_endpoint_test( + data_received_app, uri='/1', gather_request=False) + assert response.status == 413 + assert response.text == 'Payload Too Large from error_handler.' + + +@data_received_default_app.route('/1') +async def handler2(request): + return text('OK') + + +def test_payload_too_large_at_data_received_default(): + response = sanic_endpoint_test( + data_received_default_app, uri='/1', gather_request=False) + assert response.status == 413 + assert response.text == 'Error: Payload Too Large' + + +@on_header_default_app.route('/1') +async def handler3(request): + return text('OK') + + +def test_payload_too_large_at_on_header_default(): + data = 'a' * 1000 + response = sanic_endpoint_test( + on_header_default_app, method='post', uri='/1', + gather_request=False, data=data) + assert response.status == 413 + assert response.text == 'Error: Payload Too Large' From fac4bca4f49c3dc34ff57851d59fba64790d1e31 Mon Sep 17 00:00:00 2001 From: 1a23456789 Date: Tue, 6 Dec 2016 10:44:08 +0900 Subject: [PATCH 057/134] Fix test_request_timeout.py This increases sleep time, Because sometimes timeout error does not occur. --- tests/test_request_timeout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index 7b8cfb21..8cf3a680 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(1) + await asyncio.sleep(2) 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(1) + await asyncio.sleep(2) return text('OK') From 457507d8dc2772f59a144f713f01bd15f73183eb Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Wed, 7 Dec 2016 20:33:56 -0800 Subject: [PATCH 058/134] return 400 on invalid json post data --- sanic/request.py | 3 ++- tests/test_requests.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/sanic/request.py b/sanic/request.py index d3c11cd0..62d89781 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -4,6 +4,7 @@ from http.cookies import SimpleCookie from httptools import parse_url from urllib.parse import parse_qs from ujson import loads as json_loads +from sanic.exceptions import InvalidUsage from .log import log @@ -67,7 +68,7 @@ class Request(dict): try: self.parsed_json = json_loads(self.body) except Exception: - log.exception("Failed when parsing body as json") + raise InvalidUsage("Failed when parsing body as json") return self.parsed_json diff --git a/tests/test_requests.py b/tests/test_requests.py index 756113b2..81895c8c 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -49,6 +49,19 @@ def test_json(): assert results.get('test') == True +def test_invalid_json(): + app = Sanic('test_json') + + @app.route('/') + async def handler(request): + return json(request.json()) + + data = "I am not json" + request, response = sanic_endpoint_test(app, data=data) + + assert response.status == 400 + + def test_query_string(): app = Sanic('test_query_string') From 0464d31a9c91f70699b3ad5706f82927dc442623 Mon Sep 17 00:00:00 2001 From: Paul Jongsma Date: Sat, 10 Dec 2016 12:16:37 +0100 Subject: [PATCH 059/134] Find URL encoded filenames on the fs by decoding them first --- sanic/static.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sanic/static.py b/sanic/static.py index 72361a9a..ed7d6f8c 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -2,6 +2,7 @@ from aiofiles.os import stat from os import path from re import sub from time import strftime, gmtime +from urllib.parse import unquote from .exceptions import FileNotFound, InvalidUsage from .response import file, HTTPResponse @@ -38,6 +39,8 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): # from herping a derp and treating the uri as an absolute path file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ if file_uri else file_or_directory + + file_path = unquote(file_path) try: headers = {} # Check if the client has been sent this file before From 154f8570f073c94bec55f1c2681162d6b1beec16 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sun, 11 Dec 2016 13:43:31 +0200 Subject: [PATCH 060/134] add sanic aiopg example with raw sql --- examples/sanic_aiopg_example.py | 58 +++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 examples/sanic_aiopg_example.py diff --git a/examples/sanic_aiopg_example.py b/examples/sanic_aiopg_example.py new file mode 100644 index 00000000..7f4901e6 --- /dev/null +++ b/examples/sanic_aiopg_example.py @@ -0,0 +1,58 @@ +import os +import asyncio +import datetime + +import uvloop +import aiopg + +from sanic import Sanic +from sanic.response import json + +asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + +database_name = os.environ['DATABASE_NAME'] +database_host = os.environ['DATABASE_HOST'] +database_user = os.environ['DATABASE_USER'] +database_password = os.environ['DATABASE_PASSWORD'] + +connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user, database_password, database_host, database_name) +loop = asyncio.get_event_loop() + + +async def get_pool(): + return await aiopg.create_pool(connection) + +app = Sanic(name=__name__) +pool = loop.run_until_complete(get_pool()) + + +async def prepare_db(): + """ Let's create some table and add some data + + """ + async with pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute("""CREATE TABLE sanic_poll ( + id integer primary key, + question varchar(50), + pub_date timestamp + );""") + for i in range(0, 100): + await cur.execute("""INSERT INTO sanic_poll (id, question, pub_date) VALUES ({}, {}, now()) + """.format(i, i)) + + +@app.route("/") +async def handle(request): + async with pool.acquire() as conn: + async with conn.cursor() as cur: + result = [] + await cur.execute("SELECT question, pub_date FROM sanic_poll") + async for row in cur: + result.append({"question": row[0], "pub_date": row[1]}) + return json({"polls": result}) + + +if __name__ == '__main__': + loop.run_until_complete(prepare_db()) + app.run(host="0.0.0.0", port=8100, workers=3, loop=loop) From 721044b3786f4ac190256a3d0d870cf51077e2a8 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sun, 11 Dec 2016 14:04:24 +0200 Subject: [PATCH 061/134] improvements for aiopg example --- examples/sanic_aiopg_example.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/sanic_aiopg_example.py b/examples/sanic_aiopg_example.py index 7f4901e6..539917df 100644 --- a/examples/sanic_aiopg_example.py +++ b/examples/sanic_aiopg_example.py @@ -1,6 +1,5 @@ import os import asyncio -import datetime import uvloop import aiopg @@ -32,13 +31,14 @@ async def prepare_db(): """ async with pool.acquire() as conn: async with conn.cursor() as cur: - await cur.execute("""CREATE TABLE sanic_poll ( + await cur.execute('DROP TABLE IF EXISTS sanic_polls') + await cur.execute("""CREATE TABLE sanic_polls ( id integer primary key, question varchar(50), pub_date timestamp );""") for i in range(0, 100): - await cur.execute("""INSERT INTO sanic_poll (id, question, pub_date) VALUES ({}, {}, now()) + await cur.execute("""INSERT INTO sanic_polls (id, question, pub_date) VALUES ({}, {}, now()) """.format(i, i)) @@ -47,7 +47,7 @@ async def handle(request): async with pool.acquire() as conn: async with conn.cursor() as cur: result = [] - await cur.execute("SELECT question, pub_date FROM sanic_poll") + await cur.execute("SELECT question, pub_date FROM sanic_polls") async for row in cur: result.append({"question": row[0], "pub_date": row[1]}) return json({"polls": result}) From f9176bfdea547bcc3202316c3eb1fd563ace01e7 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sun, 11 Dec 2016 14:14:03 +0200 Subject: [PATCH 062/134] pep8&improvements --- examples/sanic_aiopg_example.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/sanic_aiopg_example.py b/examples/sanic_aiopg_example.py index 539917df..ff9ec65e 100644 --- a/examples/sanic_aiopg_example.py +++ b/examples/sanic_aiopg_example.py @@ -14,7 +14,10 @@ database_host = os.environ['DATABASE_HOST'] database_user = os.environ['DATABASE_USER'] database_password = os.environ['DATABASE_PASSWORD'] -connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user, database_password, database_host, database_name) +connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user, + database_password, + database_host, + database_name) loop = asyncio.get_event_loop() @@ -33,12 +36,13 @@ async def prepare_db(): async with conn.cursor() as cur: await cur.execute('DROP TABLE IF EXISTS sanic_polls') await cur.execute("""CREATE TABLE sanic_polls ( - id integer primary key, + id serial primary key, question varchar(50), pub_date timestamp );""") for i in range(0, 100): - await cur.execute("""INSERT INTO sanic_polls (id, question, pub_date) VALUES ({}, {}, now()) + await cur.execute("""INSERT INTO sanic_polls + (id, question, pub_date) VALUES ({}, {}, now()) """.format(i, i)) From b44e9baaecee1ec757409e9d1ce263f58e22fc86 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sun, 11 Dec 2016 14:21:02 +0200 Subject: [PATCH 063/134] aiopg with sqlalchemy example --- examples/sanic_aiopg_example.py | 5 +- examples/sanic_aiopg_sqlalchemy_example.py | 73 ++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 examples/sanic_aiopg_sqlalchemy_example.py diff --git a/examples/sanic_aiopg_example.py b/examples/sanic_aiopg_example.py index ff9ec65e..73ef6c64 100644 --- a/examples/sanic_aiopg_example.py +++ b/examples/sanic_aiopg_example.py @@ -1,3 +1,6 @@ +""" To run this example you need additional aiopg package + +""" import os import asyncio @@ -59,4 +62,4 @@ async def handle(request): if __name__ == '__main__': loop.run_until_complete(prepare_db()) - app.run(host="0.0.0.0", port=8100, workers=3, loop=loop) + app.run(host='0.0.0.0', port=8000, loop=loop) diff --git a/examples/sanic_aiopg_sqlalchemy_example.py b/examples/sanic_aiopg_sqlalchemy_example.py new file mode 100644 index 00000000..cb9f6c57 --- /dev/null +++ b/examples/sanic_aiopg_sqlalchemy_example.py @@ -0,0 +1,73 @@ +""" To run this example you need additional aiopg package + +""" +import os +import asyncio +import datetime + +import uvloop +from aiopg.sa import create_engine +import sqlalchemy as sa + +from sanic import Sanic +from sanic.response import json + +asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + +database_name = os.environ['DATABASE_NAME'] +database_host = os.environ['DATABASE_HOST'] +database_user = os.environ['DATABASE_USER'] +database_password = os.environ['DATABASE_PASSWORD'] + +connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user, + database_password, + database_host, + database_name) +loop = asyncio.get_event_loop() + + +metadata = sa.MetaData() + +polls = sa.Table('sanic_polls', metadata, + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('question', sa.String(50)), + sa.Column("pub_date", sa.DateTime)) + + +async def get_engine(): + return await create_engine(connection) + +app = Sanic(name=__name__) +engine = loop.run_until_complete(get_engine()) + + +async def prepare_db(): + """ Let's add some data + + """ + async with engine.acquire() as conn: + await conn.execute('DROP TABLE IF EXISTS sanic_polls') + await conn.execute("""CREATE TABLE sanic_polls ( + id serial primary key, + question varchar(50), + pub_date timestamp + );""") + for i in range(0, 100): + await conn.execute( + polls.insert().values(question=i, + pub_date=datetime.datetime.now()) + ) + + +@app.route("/") +async def handle(request): + async with engine.acquire() as conn: + result = [] + async for row in conn.execute(polls.select()): + result.append({"question": row.question, "pub_date": row.pub_date}) + return json({"polls": result}) + + +if __name__ == '__main__': + loop.run_until_complete(prepare_db()) + app.run(host='0.0.0.0', port=8000, loop=loop) From 6ef6d9a9051dca5d1896f3a03f2a8e13ce0bbc44 Mon Sep 17 00:00:00 2001 From: kamyar Date: Sun, 11 Dec 2016 16:34:22 +0200 Subject: [PATCH 064/134] url params docs typo fix add missing '>' in url params docs example --- docs/class_based_views.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/class_based_views.md b/docs/class_based_views.md index c4ceeb0c..223304ae 100644 --- a/docs/class_based_views.md +++ b/docs/class_based_views.md @@ -39,6 +39,6 @@ class NameView(HTTPMethodView): def get(self, request, name): return text('Hello {}'.format(name)) -app.add_route(NameView(), '/') ``` From 9ba2f99ea26c366aedea8f94ea0af152fcb43b99 Mon Sep 17 00:00:00 2001 From: Paul Jongsma Date: Tue, 13 Dec 2016 01:10:24 +0100 Subject: [PATCH 065/134] added a comment on why to decode the file_path --- sanic/static.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sanic/static.py b/sanic/static.py index ed7d6f8c..b02786a4 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -40,6 +40,8 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ if file_uri else file_or_directory + # URL decode the path sent by the browser otherwise we won't be able to + # match filenames which got encoded (filenames with spaces etc) file_path = unquote(file_path) try: headers = {} From 93b45e9598cfa758559b1205ea399c6198bf3c73 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 12 Dec 2016 22:18:33 -0800 Subject: [PATCH 066/134] add jinja example --- examples/jinja_example.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 examples/jinja_example.py diff --git a/examples/jinja_example.py b/examples/jinja_example.py new file mode 100644 index 00000000..1f9bb1ba --- /dev/null +++ b/examples/jinja_example.py @@ -0,0 +1,18 @@ +## To use this example: +# curl -d '{"name": "John Doe"}' localhost:8000 + +from sanic import Sanic +from sanic.response import html +from jinja2 import Template + +template = Template('Hello {{ name }}!') + +app = Sanic(__name__) + +@app.route('/') +async def test(request): + data = request.json + return html(template.render(**data)) + + +app.run(host="0.0.0.0", port=8000) From 2003eceba19618fcb20d78b19af072726113cdfc Mon Sep 17 00:00:00 2001 From: Paul Jongsma Date: Tue, 13 Dec 2016 10:41:39 +0100 Subject: [PATCH 067/134] remove trailing space --- sanic/static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/static.py b/sanic/static.py index b02786a4..a70bff2f 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -40,7 +40,7 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ if file_uri else file_or_directory - # URL decode the path sent by the browser otherwise we won't be able to + # URL decode the path sent by the browser otherwise we won't be able to # match filenames which got encoded (filenames with spaces etc) file_path = unquote(file_path) try: From 8957e4ec25fb2b6b1b20e36a78f5576b6a7736f1 Mon Sep 17 00:00:00 2001 From: Sam Agnew Date: Tue, 13 Dec 2016 12:35:46 -0500 Subject: [PATCH 068/134] Fix PEP8 in Hello World example --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2ece97d8..669ed8d9 100644 --- a/README.md +++ b/README.md @@ -33,13 +33,17 @@ All tests were run on an AWS medium instance running ubuntu, using 1 process. E from sanic import Sanic from sanic.response import json + app = Sanic() + @app.route("/") async def test(request): return json({"hello": "world"}) -app.run(host="0.0.0.0", port=8000) +if __name__ == '__main__': + app.run(host="0.0.0.0", port=8000) + ``` ## Installation From 435d5585e9d1d623b522647daf6296089dc6db85 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Wed, 14 Dec 2016 11:29:09 -0600 Subject: [PATCH 069/134] Fix leftover blank line flake8 build failed here: https://travis-ci.org/channelcat/sanic/builds/183991976 --- sanic/server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index d9136a65..a86da9fc 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -35,7 +35,6 @@ class HttpProtocol(asyncio.Protocol): # connection management '_total_request_size', '_timeout_handler', '_last_communication_time') - def __init__(self, *, loop, request_handler, error_handler, signal=Signal(), connections={}, request_timeout=60, request_max_size=None): From a9b67c3028d85fd3f95e9df11129d0cf623eb2f4 Mon Sep 17 00:00:00 2001 From: Sam Agnew Date: Wed, 14 Dec 2016 12:36:33 -0500 Subject: [PATCH 070/134] Fix quotes in sample code for consistency --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 669ed8d9..5aded700 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ app = Sanic() async def test(request): return json({"hello": "world"}) -if __name__ == '__main__': +if __name__ == "__main__": app.run(host="0.0.0.0", port=8000) ``` From 75fc9f91b942f82cd9e25161e076afa28ae10472 Mon Sep 17 00:00:00 2001 From: 38elements Date: Sun, 18 Dec 2016 09:25:39 +0900 Subject: [PATCH 071/134] Change HttpParserError process --- sanic/server.py | 8 ++++---- tests/test_bad_request.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 tests/test_bad_request.py diff --git a/sanic/server.py b/sanic/server.py index a86da9fc..9340f374 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -14,7 +14,7 @@ except ImportError: from .log import log from .request import Request -from .exceptions import RequestTimeout, PayloadTooLarge +from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage class Signal: @@ -105,9 +105,9 @@ class HttpProtocol(asyncio.Protocol): # Parse request chunk or close connection try: self.parser.feed_data(data) - except HttpParserError as e: - self.bail_out( - "Invalid request data, connection closed ({})".format(e)) + except HttpParserError: + exception = InvalidUsage('Bad Request') + self.write_error(exception) def on_url(self, url): self.url = url diff --git a/tests/test_bad_request.py b/tests/test_bad_request.py new file mode 100644 index 00000000..095f4ab1 --- /dev/null +++ b/tests/test_bad_request.py @@ -0,0 +1,20 @@ +import asyncio +from sanic import Sanic + + +def test_bad_request_response(): + app = Sanic('test_bad_request_response') + lines = [] + async def _request(sanic, loop): + connect = asyncio.open_connection('127.0.0.1', 42101) + reader, writer = await connect + writer.write(b'not http') + while True: + line = await reader.readline() + if not line: + break + lines.append(line) + app.stop() + app.run(host='127.0.0.1', port=42101, debug=False, after_start=_request) + assert lines[0] == b'HTTP/1.1 400 Bad Request\r\n' + assert lines[-1] == b'Error: Bad Request' From c657c531b482039c18bcf926052fcc1f6c8377d0 Mon Sep 17 00:00:00 2001 From: 38elements Date: Fri, 23 Dec 2016 00:13:38 +0900 Subject: [PATCH 072/134] Customizable protocol --- sanic/sanic.py | 10 ++++++---- sanic/server.py | 9 +++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 98bb230d..08894871 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -12,7 +12,7 @@ from .exceptions import Handler from .log import log, logging from .response import HTTPResponse from .router import Router -from .server import serve +from .server import serve, HttpProtocol from .static import register as static_register from .exceptions import ServerError @@ -230,14 +230,15 @@ class Sanic: # Execution # -------------------------------------------------------------------- # - def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, - after_start=None, before_stop=None, after_stop=None, sock=None, - workers=1, loop=None): + def run(self, host="127.0.0.1", port=8000, protocol=HttpProtocol, + debug=False, before_start=None, after_start=None, before_stop=None, + after_stop=None, sock=None, workers=1, loop=None): """ Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drains connections before closing. :param host: Address to host on :param port: Port to host on + :param protocol: Subclass of asyncio.Protocol :param debug: Enables debug output (slows server) :param before_start: Function to be executed before the server starts accepting connections @@ -258,6 +259,7 @@ class Sanic: self.loop = loop server_settings = { + 'protocol': protocol, 'host': host, 'port': port, 'sock': sock, diff --git a/sanic/server.py b/sanic/server.py index 9340f374..cad957f0 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -221,12 +221,13 @@ def trigger_events(events, loop): loop.run_until_complete(result) -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, +def serve(protocol, 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): """ Starts asynchronous HTTP Server on an individual process. + :param protocol: subclass of asyncio.Protocol :param host: Address to host on :param port: Port to host on :param request_handler: Sanic request handler with middleware @@ -253,7 +254,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, connections = set() signal = Signal() server = partial( - HttpProtocol, + protocol, loop=loop, connections=connections, signal=signal, From 841125570095693a7fa8af5dbef3bfb62bf68dc5 Mon Sep 17 00:00:00 2001 From: Cadel Watson Date: Fri, 23 Dec 2016 11:08:04 +1100 Subject: [PATCH 073/134] Create documentation for testing server endpoints. Currently the sanic.utils functionality is undocumented. This provides information on the interface as well as a complete example of testing a server endpoint. --- README.md | 1 + docs/testing.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 docs/testing.md diff --git a/README.md b/README.md index 5aded700..e417b4a1 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ if __name__ == "__main__": * [Class Based Views](docs/class_based_views.md) * [Cookies](docs/cookies.md) * [Static Files](docs/static_files.md) + * [Testing](docs/testing.md) * [Deploying](docs/deploying.md) * [Contributing](docs/contributing.md) * [License](LICENSE) diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 00000000..79c719e8 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,51 @@ +# Testing + +Sanic endpoints can be tested locally using the `sanic.utils` module, which +depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/) +library. The `sanic_endpoint_test` function runs a local server, issues a +configurable request to an endpoint, and returns the result. It takes the +following arguments: + +- `app` An instance of a Sanic app. +- `method` *(default `'get'`)* A string representing the HTTP method to use. +- `uri` *(default `'/'`)* A string representing the endpoint to test. +- `gather_request` *(default `True`)* A boolean which determines whether the + original request will be returned by the function. If set to `True`, the + return value is a tuple of `(request, response)`, if `False` only the + response is returned. +- `loop` *(default `None`)* The event loop to use. +- `debug` *(default `False`)* A boolean which determines whether to run the + server in debug mode. + +The function further takes the `*request_args` and `**request_kwargs`, which +are passed directly to the aiohttp ClientSession request. For example, to +supply data with a GET request, `method` would be `get` and the keyword +argument `params={'value', 'key'}` would be supplied. More information about +the available arguments to aiohttp can be found +[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session). + +Below is a complete example of an endpoint test, +using [pytest](http://doc.pytest.org/en/latest/). The test checks that the +`/challenge` endpoint responds to a GET request with a supplied challenge +string. + +```python +import pytest +import aiohttp +from sanic.utils import sanic_endpoint_test + +# Import the Sanic app, usually created with Sanic(__name__) +from external_server import app + +def test_endpoint_challenge(): + # Create the challenge data + request_data = {'challenge': 'dummy_challenge'} + + # Send the request to the endpoint, using the default `get` method + request, response = sanic_endpoint_test(app, + uri='/challenge', + params=request_data) + + # Assert that the server responds with the challenge string + assert response.text == request_data['challenge'] +``` From 5c1ef2c1cfabcadbc7d4527d05d654e7ad23ab17 Mon Sep 17 00:00:00 2001 From: Romano Bodha Date: Fri, 23 Dec 2016 01:42:05 +0100 Subject: [PATCH 074/134] Fixed import error --- docs/class_based_views.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/class_based_views.md b/docs/class_based_views.md index 223304ae..ee410b1d 100644 --- a/docs/class_based_views.md +++ b/docs/class_based_views.md @@ -6,6 +6,7 @@ Sanic has simple class based implementation. You should implement methods(get, p ```python from sanic import Sanic from sanic.views import HTTPMethodView +from sanic.response import text app = Sanic('some_name') From f091d82badc35116a45d59560f85cc6176980780 Mon Sep 17 00:00:00 2001 From: cr0hn Date: Fri, 23 Dec 2016 13:12:59 +0100 Subject: [PATCH 075/134] Improvement improvement: support fo binary data as a input. This do that the response process has more performance because not encoding needed. --- sanic/response.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index 15130edd..1f86a807 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -79,7 +79,9 @@ class HTTPResponse: self.content_type = content_type if body is not None: - self.body = body.encode('utf-8') + self.body = body + if type(body) is str: + self.body = body.encode('utf-8') else: self.body = body_bytes From 5afae986a0aeae6191a5532a11244cb7cc405f94 Mon Sep 17 00:00:00 2001 From: cr0hn Date: Fri, 23 Dec 2016 15:59:04 +0100 Subject: [PATCH 076/134] Apply response Middleware always Response middleware are useful to apply some post-process information, just before send to the user. For example: Add some HTTP headers (security headers, for example), remove "Server" banner (for security reasons) or cookie management. The change is very very simple: although an "request" middleware has produced any response, we'll even apply the response middlewares. --- sanic/sanic.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 98bb230d..f48b2bd5 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -193,18 +193,18 @@ class Sanic: if isawaitable(response): response = await response - # -------------------------------------------- # - # Response Middleware - # -------------------------------------------- # + # -------------------------------------------- # + # Response Middleware + # -------------------------------------------- # - if self.response_middleware: - for middleware in self.response_middleware: - _response = middleware(request, response) - if isawaitable(_response): - _response = await _response - if _response: - response = _response - break + if self.response_middleware: + for middleware in self.response_middleware: + _response = middleware(request, response) + if isawaitable(_response): + _response = await _response + if _response: + response = _response + break except Exception as e: # -------------------------------------------- # From 3add40625dc1b195c3129c34b88724e1a1869638 Mon Sep 17 00:00:00 2001 From: cr0hn Date: Fri, 23 Dec 2016 16:07:59 +0100 Subject: [PATCH 077/134] Explain how to chain two (or more) middlewares A funny and useful examples about how to chain middlewares. --- docs/middleware.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/middleware.md b/docs/middleware.md index 0b27443c..88d4e535 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -27,3 +27,23 @@ async def handler(request): app.run(host="0.0.0.0", port=8000) ``` + +## Middleware chain + +If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that **no return** any response in your middleware: + +```python +app = Sanic(__name__) + +@app.middleware('response') +async def custom_banner(request, response): + response.headers["Server"] = "Fake-Server" + +@app.middleware('response') +async def prevent_xss(request, response): + response.headers["x-xss-protection"] = "1; mode=block" + +app.run(host="0.0.0.0", port=8000) +``` + +The above code will apply the two middlewares in order. First the middleware **custom_banner** will change the HTTP Response headers *Server* by *Fake-Server*, and the second middleware **prevent_xss** will add the HTTP Headers for prevent Cross-Site-Scripting (XSS) attacks. From cd17a42234c6465f70795f78436e0589badc5a25 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 23 Dec 2016 09:59:28 -0800 Subject: [PATCH 078/134] Fix some verbage --- docs/middleware.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/middleware.md b/docs/middleware.md index 88d4e535..39930e3e 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -30,7 +30,7 @@ app.run(host="0.0.0.0", port=8000) ## Middleware chain -If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that **no return** any response in your middleware: +If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that you do **not return** any response in your middleware: ```python app = Sanic(__name__) From 32ea45d403ddb1e1b143ba2fbae0675f32adbc50 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Thu, 22 Dec 2016 21:00:57 -0800 Subject: [PATCH 079/134] allow overriding logging.basicConfig --- examples/override_logging.py | 23 +++++++++++++++++++++++ sanic/log.py | 2 -- sanic/sanic.py | 11 +++++++++-- tests/test_logging.py | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 examples/override_logging.py create mode 100644 tests/test_logging.py diff --git a/examples/override_logging.py b/examples/override_logging.py new file mode 100644 index 00000000..25fd78de --- /dev/null +++ b/examples/override_logging.py @@ -0,0 +1,23 @@ +from sanic import Sanic +from sanic.response import text +import json +import logging + +logging_format = "[%(asctime)s] %(process)d-%(levelname)s " +logging_format += "%(module)s::%(funcName)s():l%(lineno)d: " +logging_format += "%(message)s" + +logging.basicConfig( + format=logging_format, + level=logging.DEBUG +) +log = logging.getLogger() + +# Set logger to override default basicConfig +sanic = Sanic(logger=True) +@sanic.route("/") +def test(request): + log.info("received request; responding with 'hey'") + return text("hey") + +sanic.run(host="0.0.0.0", port=8000) diff --git a/sanic/log.py b/sanic/log.py index bd2e499e..3988bf12 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -1,5 +1,3 @@ import logging -logging.basicConfig( - level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s") log = logging.getLogger(__name__) diff --git a/sanic/sanic.py b/sanic/sanic.py index 98bb230d..c79dca43 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -6,10 +6,11 @@ from multiprocessing import Process, Event from signal import signal, SIGTERM, SIGINT from time import sleep from traceback import format_exc +import logging from .config import Config from .exceptions import Handler -from .log import log, logging +from .log import log from .response import HTTPResponse from .router import Router from .server import serve @@ -18,7 +19,13 @@ from .exceptions import ServerError class Sanic: - def __init__(self, name=None, router=None, error_handler=None): + def __init__(self, name=None, router=None, + error_handler=None, logger=None): + if logger is None: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s: %(levelname)s: %(message)s" + ) if name is None: frame_records = stack()[1] name = getmodulename(frame_records[1]) diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 00000000..65de28c2 --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,33 @@ +import asyncio +from sanic.response import text +from sanic import Sanic +from io import StringIO +from sanic.utils import sanic_endpoint_test +import logging + +logging_format = '''module: %(module)s; \ +function: %(funcName)s(); \ +message: %(message)s''' + +def test_log(): + log_stream = StringIO() + for handler in logging.root.handlers[:]: + logging.root.removeHandler(handler) + logging.basicConfig( + format=logging_format, + level=logging.DEBUG, + stream=log_stream + ) + log = logging.getLogger() + app = Sanic('test_logging', logger=True) + @app.route('/') + def handler(request): + log.info('hello world') + return text('hello') + + request, response = sanic_endpoint_test(app) + log_text = log_stream.getvalue().strip().split('\n')[-3] + assert log_text == "module: test_logging; function: handler(); message: hello world" + +if __name__ =="__main__": + test_log() From 39211f8fbddb997f001f8ffc2059a0a0ad729125 Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 24 Dec 2016 11:40:07 +0900 Subject: [PATCH 080/134] Refactor arguments of serve function --- sanic/server.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index cad957f0..74e834b4 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -221,26 +221,31 @@ def trigger_events(events, loop): loop.run_until_complete(result) -def serve(protocol, 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): +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, protocol=HttpProtocol): """ Starts asynchronous HTTP Server on an individual process. - :param protocol: subclass of asyncio.Protocol :param host: Address to host on :param port: Port to host on :param request_handler: Sanic request handler with middleware + :param error_handler: Sanic error handler with middleware + :param before_start: Function to be executed before the server starts + listening. Takes single argument `loop` :param after_start: Function to be executed after the server starts listening. Takes single argument `loop` :param before_stop: Function to be executed when a stop signal is received before it is respected. Takes single argumenet `loop` + :param after_stop: Function to be executed when a stop signal is + received after it is respected. Takes single argumenet `loop` :param debug: Enables debug output (slows server) :param request_timeout: time in seconds :param sock: Socket for the server to accept connections from :param request_max_size: size in bytes, `None` for no limit :param reuse_port: `True` for multiple workers :param loop: asyncio compatible event loop + :param protocol: Subclass of asyncio.Protocol :return: Nothing """ loop = loop or async_loop.new_event_loop() From 2f0a582aa782f224e59911fa94d2f45c8c761e18 Mon Sep 17 00:00:00 2001 From: Konstantin Hantsov Date: Sat, 24 Dec 2016 10:28:34 +0100 Subject: [PATCH 081/134] Make golang performance test return JSON instead of string --- tests/performance/golang/golang.http.go | 26 +++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/tests/performance/golang/golang.http.go b/tests/performance/golang/golang.http.go index fb13cc8b..5aeedb61 100644 --- a/tests/performance/golang/golang.http.go +++ b/tests/performance/golang/golang.http.go @@ -1,16 +1,30 @@ package main import ( - "fmt" - "os" - "net/http" + "encoding/json" + "net/http" + "os" ) +type TestJSONResponse struct { + Test bool +} + func handler(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:]) + response := TestJSONResponse{true} + + js, err := json.Marshal(response) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(js) } func main() { - http.HandleFunc("/", handler) - http.ListenAndServe(":" + os.Args[1], nil) + http.HandleFunc("/", handler) + http.ListenAndServe(":"+os.Args[1], nil) } From 2d05243c4a2a6d10a868cacddc103bbd25921d0b Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 24 Dec 2016 22:49:48 +0900 Subject: [PATCH 082/134] Refactor arguments of run function --- sanic/sanic.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 08894871..87f12ac3 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -230,15 +230,14 @@ class Sanic: # Execution # -------------------------------------------------------------------- # - def run(self, host="127.0.0.1", port=8000, protocol=HttpProtocol, - debug=False, before_start=None, after_start=None, before_stop=None, - after_stop=None, sock=None, workers=1, loop=None): + def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, + after_start=None, before_stop=None, after_stop=None, sock=None, + workers=1, loop=None, protocol=HttpProtocol): """ Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drains connections before closing. :param host: Address to host on :param port: Port to host on - :param protocol: Subclass of asyncio.Protocol :param debug: Enables debug output (slows server) :param before_start: Function to be executed before the server starts accepting connections @@ -252,6 +251,7 @@ class Sanic: :param workers: Number of processes received before it is respected :param loop: asyncio compatible event loop + :param protocol: Subclass of asyncio.Protocol :return: Nothing """ self.error_handler.debug = True From cc982c5a61ef9523e3565b9d3d014f404d4141ec Mon Sep 17 00:00:00 2001 From: cr0hn Date: Sat, 24 Dec 2016 15:24:25 +0100 Subject: [PATCH 083/134] Update response.py Type check by isinstance --- sanic/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index 1f86a807..c09c8dbd 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -80,7 +80,7 @@ class HTTPResponse: if body is not None: self.body = body - if type(body) is str: + if isinstance(body, str): self.body = body.encode('utf-8') else: self.body = body_bytes From 74f305cfb78bc23e2dd647d83e26d658f29d1e19 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 14:06:53 -0800 Subject: [PATCH 084/134] Adds python36 to tox.ini and .travis.yml --- .travis.yml | 1 + tox.ini | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 942a5df2..5e41a68e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: python python: - '3.5' + - '3.6' install: - pip install -r requirements.txt - pip install -r requirements-dev.txt diff --git a/tox.ini b/tox.ini index 258395ed..ecb7ca87 100644 --- a/tox.ini +++ b/tox.ini @@ -1,27 +1,30 @@ [tox] -envlist = py35, report +envlist = py35, py36 [testenv] deps = aiohttp pytest - # pytest-cov coverage commands = - coverage run -m pytest tests {posargs} + coverage run -m pytest -v tests {posargs} mv .coverage .coverage.{envname} -basepython: - py35: python3.5 - whitelist_externals = coverage mv echo +[testenv:flake8] +deps = + flake8 + +commands = + flake8 sanic + [testenv:report] commands = @@ -29,6 +32,3 @@ commands = coverage report coverage html echo "Open file://{toxinidir}/coverage/index.html" - -basepython = - python3.5 \ No newline at end of file From c2622511ce94f4a46599c23e6540128c65daacaf Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 13 Dec 2016 12:20:16 -0800 Subject: [PATCH 085/134] Raise error if response is malformed. Issue #115 --- sanic/server.py | 6 ++++-- tests/test_requests.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 9340f374..11756005 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -6,6 +6,7 @@ from signal import SIGINT, SIGTERM from time import time from httptools import HttpRequestParser from httptools.parser.errors import HttpParserError +from .exceptions import ServerError try: import uvloop as async_loop @@ -173,8 +174,9 @@ class HttpProtocol(asyncio.Protocol): "Writing error failed, connection closed {}".format(e)) def bail_out(self, message): - log.debug(message) - self.transport.close() + exception = ServerError(message) + self.write_error(exception) + log.error(message) def cleanup(self): self.parser = None diff --git a/tests/test_requests.py b/tests/test_requests.py index 81895c8c..5895e3d5 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -2,6 +2,7 @@ from json import loads as json_loads, dumps as json_dumps from sanic import Sanic from sanic.response import json, text from sanic.utils import sanic_endpoint_test +from sanic.exceptions import ServerError # ------------------------------------------------------------ # @@ -32,6 +33,22 @@ def test_text(): assert response.text == 'Hello' +def test_invalid_response(): + app = Sanic('test_invalid_response') + + @app.exception(ServerError) + def handler_exception(request, exception): + return text('Internal Server Error.', 500) + + @app.route('/') + async def handler(request): + return 'This should fail' + + request, response = sanic_endpoint_test(app) + assert response.status == 500 + assert response.text == "Internal Server Error." + + def test_json(): app = Sanic('test_json') From 29f3c22fede7716cdebd06b8f4f44c48dfb0814e Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:11:12 -0800 Subject: [PATCH 086/134] Rework conditionals to not be inline --- sanic/static.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sanic/static.py b/sanic/static.py index a70bff2f..e39dd76f 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -33,12 +33,14 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): # served. os.path.realpath seems to be very slow if file_uri and '../' in file_uri: raise InvalidUsage("Invalid URL") - + # Merge served directory and requested file if provided # Strip all / that in the beginning of the URL to help prevent python # from herping a derp and treating the uri as an absolute path - file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ - if file_uri else file_or_directory + file_path = file_or_directory + if file_uri: + file_path = path.join( + file_or_directory, sub('^[/]*', '', file_uri)) # URL decode the path sent by the browser otherwise we won't be able to # match filenames which got encoded (filenames with spaces etc) From 16182472fa73b6b5035ce4904bbb3edf3e1bf8a8 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:11:46 -0800 Subject: [PATCH 087/134] Remove trailing whitespace --- sanic/static.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sanic/static.py b/sanic/static.py index e39dd76f..9f5f2d52 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -33,7 +33,6 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): # served. os.path.realpath seems to be very slow if file_uri and '../' in file_uri: raise InvalidUsage("Invalid URL") - # Merge served directory and requested file if provided # Strip all / that in the beginning of the URL to help prevent python # from herping a derp and treating the uri as an absolute path From 8be849cc40dc5f8f55536c462f9dff0c657f6a0f Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:16:19 -0800 Subject: [PATCH 088/134] Rewrite static files tests Relates to PR #188 Changes include: - Rewriting to work with pytest fixtures and an actual static directory - Addition of a test that covers file paths that must be unquoted as a uri --- tests/static/decode me.txt | 1 + tests/static/test.file | 1 + tests/test_static.py | 60 +++++++++++++++++++++++++++++--------- 3 files changed, 48 insertions(+), 14 deletions(-) create mode 100644 tests/static/decode me.txt create mode 100644 tests/static/test.file diff --git a/tests/static/decode me.txt b/tests/static/decode me.txt new file mode 100644 index 00000000..e5d05ac1 --- /dev/null +++ b/tests/static/decode me.txt @@ -0,0 +1 @@ +I need to be decoded as a uri diff --git a/tests/static/test.file b/tests/static/test.file new file mode 100644 index 00000000..0725a6ef --- /dev/null +++ b/tests/static/test.file @@ -0,0 +1 @@ +I am just a regular static file diff --git a/tests/test_static.py b/tests/test_static.py index 6dafac2b..82b0d1f9 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -1,30 +1,62 @@ import inspect import os +import pytest + from sanic import Sanic from sanic.utils import sanic_endpoint_test -def test_static_file(): - current_file = inspect.getfile(inspect.currentframe()) - with open(current_file, 'rb') as file: - current_file_contents = file.read() +@pytest.fixture(scope='module') +def static_file_directory(): + """The static directory to serve""" + current_file = inspect.getfile(inspect.currentframe()) + current_directory = os.path.dirname(os.path.abspath(current_file)) + static_directory = os.path.join(current_directory, 'static') + return static_directory + + +@pytest.fixture(scope='module') +def static_file_path(static_file_directory): + """The path to the static file that we want to serve""" + return os.path.join(static_file_directory, 'test.file') + + +@pytest.fixture(scope='module') +def static_file_content(static_file_path): + """The content of the static file to check""" + with open(static_file_path, 'rb') as file: + return file.read() + + +def test_static_file(static_file_path, static_file_content): app = Sanic('test_static') - app.static('/testing.file', current_file) + app.static('/testing.file', static_file_path) request, response = sanic_endpoint_test(app, uri='/testing.file') assert response.status == 200 - assert response.body == current_file_contents + assert response.body == static_file_content -def test_static_directory(): - current_file = inspect.getfile(inspect.currentframe()) - current_directory = os.path.dirname(os.path.abspath(current_file)) - with open(current_file, 'rb') as file: - current_file_contents = file.read() + +def test_static_directory( + static_file_directory, static_file_path, static_file_content): app = Sanic('test_static') - app.static('/dir', current_directory) + app.static('/dir', static_file_directory) - request, response = sanic_endpoint_test(app, uri='/dir/test_static.py') + request, response = sanic_endpoint_test(app, uri='/dir/test.file') assert response.status == 200 - assert response.body == current_file_contents \ No newline at end of file + assert response.body == static_file_content + + +def test_static_url_decode_file(static_file_directory): + decode_me_path = os.path.join(static_file_directory, 'decode me.txt') + with open(decode_me_path, 'rb') as file: + decode_me_contents = file.read() + + app = Sanic('test_static') + app.static('/dir', static_file_directory) + + request, response = sanic_endpoint_test(app, uri='/dir/decode me.txt') + assert response.status == 200 + assert response.body == decode_me_contents From d7e94473f3c350a1c3ec49e79f3a7e6c711b8842 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:37:55 -0800 Subject: [PATCH 089/134] Use a try/except, it's a bit faster Also reorder some imports and add some comments --- sanic/response.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index c09c8dbd..2c4c7f27 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,9 +1,11 @@ from aiofiles import open as open_async -from .cookies import CookieJar from mimetypes import guess_type from os import path + from ujson import dumps as json_dumps +from .cookies import CookieJar + COMMON_STATUS_CODES = { 200: b'OK', 400: b'Bad Request', @@ -79,9 +81,12 @@ class HTTPResponse: self.content_type = content_type if body is not None: - self.body = body - if isinstance(body, str): + try: + # Try to encode it regularly self.body = body.encode('utf-8') + except AttributeError: + # Convert it to a str if you can't + self.body = str(body).encode('utf-8') else: self.body = body_bytes From f1f38c24da6d845701801c6f55ba9e5f24fd6acb Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:47:15 -0800 Subject: [PATCH 090/134] Add test for PR: #215 --- tests/test_response.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/test_response.py diff --git a/tests/test_response.py b/tests/test_response.py new file mode 100644 index 00000000..f35f10e9 --- /dev/null +++ b/tests/test_response.py @@ -0,0 +1,18 @@ +from random import choice + +from sanic import Sanic +from sanic.response import HTTPResponse +from sanic.utils import sanic_endpoint_test + + +def test_response_body_not_a_string(): + """Test when a response body sent from the application is not a string""" + app = Sanic('response_body_not_a_string') + random_num = choice(range(1000)) + + @app.route('/hello') + async def hello_route(request): + return HTTPResponse(body=random_num) + + request, response = sanic_endpoint_test(app, uri='/hello') + assert response.text == str(random_num) From cf7616ebe5216007699cb264667079a3c739e29a Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:51:16 -0800 Subject: [PATCH 091/134] Increment version to 0.1.9 --- sanic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/__init__.py b/sanic/__init__.py index 6e7f8d23..6b9b3a80 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1,6 +1,6 @@ from .sanic import Sanic from .blueprints import Blueprint -__version__ = '0.1.8' +__version__ = '0.1.9' __all__ = ['Sanic', 'Blueprint'] From 7e6c92dc52525cdf30babc9f40125cddafab8b43 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 13 Dec 2016 21:24:26 -0800 Subject: [PATCH 092/134] convert header values to strings --- sanic/response.py | 4 +++- tests/test_requests.py | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index 15130edd..f4e24e99 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,5 +1,5 @@ from aiofiles import open as open_async -from .cookies import CookieJar +from .cookies import CookieJar, Cookie from mimetypes import guess_type from os import path from ujson import dumps as json_dumps @@ -97,6 +97,8 @@ class HTTPResponse: headers = b'' if self.headers: headers = b''.join( + b'%b: %b\r\n' % (name.encode(), str(value).encode('utf-8')) + if type(value) != Cookie else b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) for name, value in self.headers.items() ) diff --git a/tests/test_requests.py b/tests/test_requests.py index 81895c8c..a8ab26ad 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -32,6 +32,32 @@ def test_text(): assert response.text == 'Hello' +def test_headers(): + app = Sanic('test_text') + + @app.route('/') + async def handler(request): + headers = {"spam": "great"} + return text('Hello', headers=headers) + + request, response = sanic_endpoint_test(app) + + assert response.headers.get('spam') == 'great' + + +def test_invalid_headers(): + app = Sanic('test_text') + + @app.route('/') + async def handler(request): + headers = {"answer": 42} + return text('Hello', headers=headers) + + request, response = sanic_endpoint_test(app) + + assert response.headers.get('answer') == '42' + + def test_json(): app = Sanic('test_json') From 00b5a496dd8d6ddc13632aa357b25c525001786a Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 17 Dec 2016 21:15:20 -0800 Subject: [PATCH 093/134] type -> isinstance --- sanic/response.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index f4e24e99..d5b2beae 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -97,9 +97,10 @@ class HTTPResponse: headers = b'' if self.headers: headers = b''.join( - b'%b: %b\r\n' % (name.encode(), str(value).encode('utf-8')) - if type(value) != Cookie else b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) + if isinstance(value, str) or isinstance(value, Cookie) + else b'%b: %b\r\n' % (name.encode(), + str(value).encode('utf-8')) for name, value in self.headers.items() ) From 7d7cbaacf1505a02b2e2a652cb32f212b3d3a64b Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 17 Dec 2016 21:32:48 -0800 Subject: [PATCH 094/134] header format function --- sanic/response.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index d5b2beae..21add8f6 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -94,13 +94,15 @@ class HTTPResponse: if keep_alive and keep_alive_timeout: timeout_header = b'Keep-Alive: timeout=%d\r\n' % keep_alive_timeout + format_headers = lambda name, value: b'%b: %b\r\n' %\ + (name.encode(), value.encode('utf-8')) + headers = b'' if self.headers: headers = b''.join( - b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) + format_headers(name, value) if isinstance(value, str) or isinstance(value, Cookie) - else b'%b: %b\r\n' % (name.encode(), - str(value).encode('utf-8')) + else format_headers(name, str(value)) for name, value in self.headers.items() ) From be9eca2d63845df1dab69133b1db88f264f93ac9 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 24 Dec 2016 20:56:07 -0800 Subject: [PATCH 095/134] use try/except --- sanic/response.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 21add8f6..1202b101 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -87,6 +87,15 @@ class HTTPResponse: self.headers = headers or {} self._cookies = None + @staticmethod + def format_header(name, value): + try: + return b'%b: %b\r\n' %\ + (name.encode(), value.encode('utf-8')) + except: + return b'%b: %b\r\n' %\ + (name.encode(), str(value).encode('utf-8')) + def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): # This is all returned in a kind-of funky way # We tried to make this as fast as possible in pure python @@ -94,15 +103,10 @@ class HTTPResponse: if keep_alive and keep_alive_timeout: timeout_header = b'Keep-Alive: timeout=%d\r\n' % keep_alive_timeout - format_headers = lambda name, value: b'%b: %b\r\n' %\ - (name.encode(), value.encode('utf-8')) - headers = b'' if self.headers: headers = b''.join( - format_headers(name, value) - if isinstance(value, str) or isinstance(value, Cookie) - else format_headers(name, str(value)) + self.format_header(name, value) for name, value in self.headers.items() ) From 56d6c2a92910b160a3519c0402721323a21218dc Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sun, 25 Dec 2016 18:55:25 -0800 Subject: [PATCH 096/134] Change travis job to use tox --- .travis.yml | 10 ++-------- tox.ini | 4 +++- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5e41a68e..a215a57b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,14 +2,8 @@ language: python python: - '3.5' - '3.6' -install: - - pip install -r requirements.txt - - pip install -r requirements-dev.txt - - python setup.py install - - pip install flake8 - - pip install pytest -before_script: flake8 sanic -script: py.test -v tests +install: pip install tox-travis +script: tox deploy: provider: pypi user: channelcat diff --git a/tox.ini b/tox.ini index ecb7ca87..222e17c6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] -envlist = py35, py36 +envlist = py35, py36, flake8, report [testenv] @@ -26,6 +26,8 @@ commands = flake8 sanic [testenv:report] +deps= + coverage commands = coverage combine From 15578547553254728b9cd4f9fd46d92ab4d0ffe0 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sun, 25 Dec 2016 19:05:11 -0800 Subject: [PATCH 097/134] Update to make flake8 actually work --- .travis.yml | 1 + tox.ini | 27 ++++++++------------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/.travis.yml b/.travis.yml index a215a57b..1b31c4f3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +sudo: false language: python python: - '3.5' diff --git a/tox.ini b/tox.ini index 222e17c6..a2f89206 100644 --- a/tox.ini +++ b/tox.ini @@ -1,22 +1,21 @@ [tox] -envlist = py35, py36, flake8, report +envlist = py35, py36, flake8 + +[travis] + +python = + 3.5: py35, flake8 + 3.6: py36, flake8 [testenv] deps = aiohttp pytest - coverage commands = - coverage run -m pytest -v tests {posargs} - mv .coverage .coverage.{envname} - -whitelist_externals = - coverage - mv - echo + pytest tests {posargs} [testenv:flake8] deps = @@ -24,13 +23,3 @@ deps = commands = flake8 sanic - -[testenv:report] -deps= - coverage - -commands = - coverage combine - coverage report - coverage html - echo "Open file://{toxinidir}/coverage/index.html" From 01b42fb39946d6ad93ce622fba14ef81a5d9492d Mon Sep 17 00:00:00 2001 From: Hyunjun Kim Date: Mon, 26 Dec 2016 20:37:16 +0900 Subject: [PATCH 098/134] Allow Sanic-inherited application --- sanic/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/__main__.py b/sanic/__main__.py index 8bede98f..8653cd55 100644 --- a/sanic/__main__.py +++ b/sanic/__main__.py @@ -20,7 +20,7 @@ if __name__ == "__main__": module = import_module(module_name) app = getattr(module, app_name, None) - if type(app) is not Sanic: + if not isinstance(app, Sanic): raise ValueError("Module is not a Sanic app, it is a {}. " "Perhaps you meant {}.app?" .format(type(app).__name__, args.module)) From 986b0aa106702ffc015044b55746b8eb7d86ef9e Mon Sep 17 00:00:00 2001 From: Sean Parsons Date: Mon, 26 Dec 2016 06:41:41 -0500 Subject: [PATCH 099/134] Added token property to request object. --- sanic/request.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/sanic/request.py b/sanic/request.py index 62d89781..4309bbed 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -72,6 +72,17 @@ class Request(dict): return self.parsed_json + @property + def token(self): + """ + Attempts to return the auth header token. + :return: token related to request + """ + auth_header = self.headers.get('Authorization') + if auth_header is not None: + return auth_header.split()[1] + return auth_header + @property def form(self): if self.parsed_form is None: From 548458c3e0e8201434dd9160cbfb4b151010eaf4 Mon Sep 17 00:00:00 2001 From: Sean Parsons Date: Mon, 26 Dec 2016 06:48:53 -0500 Subject: [PATCH 100/134] Added test for new token property on request object. --- tests/test_requests.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_requests.py b/tests/test_requests.py index 5895e3d5..4f81b9a0 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -92,6 +92,24 @@ def test_query_string(): assert request.args.get('test2') == 'false' +def test_token(): + app = Sanic('test_post_token') + + @app.route('/') + async def handler(request): + return text('OK') + + # uuid4 generated token. + token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf' + headers = { + 'content-type': 'application/json', + 'Authorization': 'Token {}'.format(token) + } + + request, response = sanic_endpoint_test(app, headers=headers) + + assert request.token == token + # ------------------------------------------------------------ # # POST # ------------------------------------------------------------ # From ac44900fc40f3296ba858eaf371a1213339f2db2 Mon Sep 17 00:00:00 2001 From: 38elements Date: Mon, 26 Dec 2016 23:41:10 +0900 Subject: [PATCH 101/134] Add test and example for custom protocol --- examples/custom_protocol.py | 24 ++++++++++++++++++++++++ sanic/utils.py | 6 +++--- tests/test_custom_protocol.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 examples/custom_protocol.py create mode 100644 tests/test_custom_protocol.py diff --git a/examples/custom_protocol.py b/examples/custom_protocol.py new file mode 100644 index 00000000..b5e20ee6 --- /dev/null +++ b/examples/custom_protocol.py @@ -0,0 +1,24 @@ +from sanic import Sanic +from sanic.server import HttpProtocol +from sanic.response import text + +app = Sanic(__name__) + + +class CustomHttpProtocol(HttpProtocol): + + def write_response(self, response): + if isinstance(response, str): + response = text(response) + self.transport.write( + response.output(self.request.version) + ) + self.transport.close() + + +@app.route("/") +async def test(request): + return 'Hello, world!' + + +app.run(host="0.0.0.0", port=8000, protocol=CustomHttpProtocol) diff --git a/sanic/utils.py b/sanic/utils.py index 88444b3c..9f4a97d8 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -16,8 +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, server_kwargs={}, + *request_args, **request_kwargs): results = [] exceptions = [] @@ -36,7 +36,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, app.stop() app.run(host=HOST, debug=debug, port=42101, - after_start=_collect_response, loop=loop) + after_start=_collect_response, loop=loop, **server_kwargs) if exceptions: raise ValueError("Exception during request: {}".format(exceptions)) diff --git a/tests/test_custom_protocol.py b/tests/test_custom_protocol.py new file mode 100644 index 00000000..88202428 --- /dev/null +++ b/tests/test_custom_protocol.py @@ -0,0 +1,32 @@ +from sanic import Sanic +from sanic.server import HttpProtocol +from sanic.response import text +from sanic.utils import sanic_endpoint_test + +app = Sanic('test_custom_porotocol') + + +class CustomHttpProtocol(HttpProtocol): + + def write_response(self, response): + if isinstance(response, str): + response = text(response) + self.transport.write( + response.output(self.request.version) + ) + self.transport.close() + + +@app.route('/1') +async def handler_1(request): + return 'OK' + + +def test_use_custom_protocol(): + server_kwargs = { + 'protocol': CustomHttpProtocol + } + request, response = sanic_endpoint_test(app, uri='/1', + server_kwargs=server_kwargs) + assert response.status == 200 + assert response.text == 'OK' From 39b279f0f2aa20b90e15cb569535207341f303eb Mon Sep 17 00:00:00 2001 From: 38elements Date: Mon, 26 Dec 2016 23:54:59 +0900 Subject: [PATCH 102/134] Improve examples/custom_protocol.py --- examples/custom_protocol.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/custom_protocol.py b/examples/custom_protocol.py index b5e20ee6..d1df8fde 100644 --- a/examples/custom_protocol.py +++ b/examples/custom_protocol.py @@ -16,9 +16,13 @@ class CustomHttpProtocol(HttpProtocol): self.transport.close() -@app.route("/") -async def test(request): - return 'Hello, world!' +@app.route('/') +async def string(request): + return 'string' -app.run(host="0.0.0.0", port=8000, protocol=CustomHttpProtocol) +@app.route('/1') +async def response(request): + return text('response') + +app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol) From a4f77984b79e52441c07557e05ccc86cd2e82727 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 26 Dec 2016 14:37:05 -0800 Subject: [PATCH 103/134] stop multiple worker server without sleep loop; issue #73 --- sanic/sanic.py | 5 ++--- tests/test_multiprocessing.py | 26 +++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index ecf5b652..033b1e9d 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -3,8 +3,8 @@ from collections import deque from functools import partial from inspect import isawaitable, stack, getmodulename from multiprocessing import Process, Event +from select import select from signal import signal, SIGTERM, SIGINT -from time import sleep from traceback import format_exc import logging @@ -352,8 +352,7 @@ class Sanic: # Infinitely wait for the stop event try: - while not stop_event.is_set(): - sleep(0.3) + select(stop_event) except: pass diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index 545ecee7..cc967ef1 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -1,5 +1,5 @@ from multiprocessing import Array, Event, Process -from time import sleep +from time import sleep, time from ujson import loads as json_loads from sanic import Sanic @@ -51,3 +51,27 @@ def skip_test_multiprocessing(): raise ValueError("Expected JSON response but got '{}'".format(response)) assert results.get('test') == True + + +def test_drain_connections(): + app = Sanic('test_json') + + @app.route('/') + async def handler(request): + return json({"test": True}) + + stop_event = Event() + async def after_start(*args, **kwargs): + http_response = await local_request('get', '/') + stop_event.set() + + start = time() + app.serve_multiple({ + 'host': HOST, + 'port': PORT, + 'after_start': after_start, + 'request_handler': app.handle_request, + }, workers=2, stop_event=stop_event) + end = time() + + assert end - start < 0.05 From 15e7d8ab2eac51a4b0858efe762403de3d4bed2b Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 28 Dec 2016 18:03:12 +0900 Subject: [PATCH 104/134] Handle hooks parameters in more debuggable way 1. not list() -> callable() The args of hooking parameters of Sanic have to be callables. For wrong parameters, errors will be generated from: ``` listeners += args ``` By checking just list type, the raised error will be associated with `[args]` instead of `args`, which is not given by users. With this patch, the raised error will be associated with `args`. Then users can notice their argument was neither callable nor list in the easier way. 2. Function -> Functions in document Regarding the parameter as a list is harmless to the user code. But unawareness of its type can be list can limit the potent of the user code. --- sanic/sanic.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index ecf5b652..7ec3260e 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -246,13 +246,13 @@ class Sanic: :param host: Address to host on :param port: Port to host on :param debug: Enables debug output (slows server) - :param before_start: Function to be executed before the server starts + :param before_start: Functions to be executed before the server starts accepting connections - :param after_start: Function to be executed after the server starts + :param after_start: Functions to be executed after the server starts accepting connections - :param before_stop: Function to be executed when a stop signal is + :param before_stop: Functions to be executed when a stop signal is received before it is respected - :param after_stop: Function to be executed when all requests are + :param after_stop: Functions to be executed when all requests are complete :param sock: Socket for the server to accept connections from :param workers: Number of processes @@ -290,7 +290,7 @@ class Sanic: for blueprint in self.blueprints.values(): listeners += blueprint.listeners[event_name] if args: - if type(args) is not list: + if callable(args): args = [args] listeners += args if reverse: From 83e9d0885373e6d5f43a328e861d2321c769e62b Mon Sep 17 00:00:00 2001 From: 38elements Date: Thu, 29 Dec 2016 13:11:27 +0900 Subject: [PATCH 105/134] Add document for custom protocol --- README.md | 1 + docs/custom_protocol.md | 68 +++++++++++++++++++++++++++++++++++++ examples/custom_protocol.py | 28 --------------- sanic/sanic.py | 2 +- sanic/server.py | 2 +- 5 files changed, 71 insertions(+), 30 deletions(-) create mode 100644 docs/custom_protocol.md delete mode 100644 examples/custom_protocol.py diff --git a/README.md b/README.md index 5aded700..61b154ff 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ if __name__ == "__main__": * [Class Based Views](docs/class_based_views.md) * [Cookies](docs/cookies.md) * [Static Files](docs/static_files.md) + * [Custom Protocol](docs/custom_protocol.md) * [Deploying](docs/deploying.md) * [Contributing](docs/contributing.md) * [License](LICENSE) diff --git a/docs/custom_protocol.md b/docs/custom_protocol.md new file mode 100644 index 00000000..a92f1b53 --- /dev/null +++ b/docs/custom_protocol.md @@ -0,0 +1,68 @@ +# Custom Protocol + +You can change the behavior of protocol by using custom protocol. +If you want to use custom protocol, you should put subclass of [protocol class](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes) in the protocol keyword argument of `sanic.run()`. The constructor of custom protocol class gets following keyword arguments from Sanic. + +* loop +`loop` is an asyncio compatible event loop. + +* connections +`connections` is a `set object` to store protocol objects. +When Sanic receives `SIGINT` or `SIGTERM`, Sanic executes `protocol.close_if_idle()` for a `protocol objects` stored in connections. + +* signal +`signal` is a `sanic.server.Signal object` with `stopped attribute`. +When Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` becomes `True`. + +* request_handler +`request_handler` is a coroutine that takes a `sanic.request.Request` object and a `response callback` as arguments. + +* error_handler +`error_handler` is a `sanic.exceptions.Handler` object. + +* request_timeout +`request_timeout` is seconds for timeout. + +* request_max_size +`request_max_size` is bytes of max request size. + +## Example + +```python +from sanic import Sanic +from sanic.server import HttpProtocol +from sanic.response import text + +app = Sanic(__name__) + + +class CustomHttpProtocol(HttpProtocol): + + def __init__(self, *, loop, request_handler, error_handler, + signal, connections, request_timeout, request_max_size): + super().__init__( + loop=loop, request_handler=request_handler, + error_handler=error_handler, signal=signal, + connections=connections, request_timeout=request_timeout, + request_max_size=request_max_size) + + def write_response(self, response): + if isinstance(response, str): + response = text(response) + self.transport.write( + response.output(self.request.version) + ) + self.transport.close() + + +@app.route('/') +async def string(request): + return 'string' + + +@app.route('/1') +async def response(request): + return text('response') + +app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol) +``` diff --git a/examples/custom_protocol.py b/examples/custom_protocol.py deleted file mode 100644 index d1df8fde..00000000 --- a/examples/custom_protocol.py +++ /dev/null @@ -1,28 +0,0 @@ -from sanic import Sanic -from sanic.server import HttpProtocol -from sanic.response import text - -app = Sanic(__name__) - - -class CustomHttpProtocol(HttpProtocol): - - def write_response(self, response): - if isinstance(response, str): - response = text(response) - self.transport.write( - response.output(self.request.version) - ) - self.transport.close() - - -@app.route('/') -async def string(request): - return 'string' - - -@app.route('/1') -async def response(request): - return text('response') - -app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol) diff --git a/sanic/sanic.py b/sanic/sanic.py index 87f12ac3..e3115a69 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -251,7 +251,7 @@ class Sanic: :param workers: Number of processes received before it is respected :param loop: asyncio compatible event loop - :param protocol: Subclass of asyncio.Protocol + :param protocol: Subclass of asyncio protocol class :return: Nothing """ self.error_handler.debug = True diff --git a/sanic/server.py b/sanic/server.py index 74e834b4..ffaf2d22 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -245,7 +245,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, :param request_max_size: size in bytes, `None` for no limit :param reuse_port: `True` for multiple workers :param loop: asyncio compatible event loop - :param protocol: Subclass of asyncio.Protocol + :param protocol: Subclass of asyncio protocol class :return: Nothing """ loop = loop or async_loop.new_event_loop() From 6bb4dae5e041e38065f403eea759baeea12067d0 Mon Sep 17 00:00:00 2001 From: 38elements Date: Thu, 29 Dec 2016 13:25:04 +0900 Subject: [PATCH 106/134] Fix format in custom_protocol.md --- docs/custom_protocol.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/custom_protocol.md b/docs/custom_protocol.md index a92f1b53..d4f14dee 100644 --- a/docs/custom_protocol.md +++ b/docs/custom_protocol.md @@ -1,29 +1,29 @@ # Custom Protocol -You can change the behavior of protocol by using custom protocol. +You can change the behavior of protocol by using custom protocol. If you want to use custom protocol, you should put subclass of [protocol class](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes) in the protocol keyword argument of `sanic.run()`. The constructor of custom protocol class gets following keyword arguments from Sanic. -* loop +* loop `loop` is an asyncio compatible event loop. -* connections +* connections `connections` is a `set object` to store protocol objects. When Sanic receives `SIGINT` or `SIGTERM`, Sanic executes `protocol.close_if_idle()` for a `protocol objects` stored in connections. -* signal +* signal `signal` is a `sanic.server.Signal object` with `stopped attribute`. When Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` becomes `True`. -* request_handler +* request_handler `request_handler` is a coroutine that takes a `sanic.request.Request` object and a `response callback` as arguments. -* error_handler +* error_handler `error_handler` is a `sanic.exceptions.Handler` object. -* request_timeout +* request_timeout `request_timeout` is seconds for timeout. -* request_max_size +* request_max_size `request_max_size` is bytes of max request size. ## Example From 64e0e2d19f74af9b50da86f12a151a2304de0cd1 Mon Sep 17 00:00:00 2001 From: 38elements Date: Thu, 29 Dec 2016 16:41:04 +0900 Subject: [PATCH 107/134] Improve custom_protocol.md --- docs/custom_protocol.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/custom_protocol.md b/docs/custom_protocol.md index d4f14dee..7381a3cb 100644 --- a/docs/custom_protocol.md +++ b/docs/custom_protocol.md @@ -27,6 +27,8 @@ When Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` becomes `True`. `request_max_size` is bytes of max request size. ## Example +By default protocol, an error occurs, if the handler does not return an `HTTPResponse object`. +In this example, By rewriting `write_response()`, if the handler returns `str`, it will be converted to an `HTTPResponse object`. ```python from sanic import Sanic From e7314d17753b9d566ab85c66c059a98e1d7f0d6e Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Thu, 29 Dec 2016 19:22:11 +0200 Subject: [PATCH 108/134] fix misprints&renaming --- docs/routing.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/routing.md b/docs/routing.md index d15ba4e9..92ac2290 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -33,12 +33,12 @@ async def handler1(request): return text('OK') app.add_route(handler1, '/test') -async def handler(request, name): +async def handler2(request, name): return text('Folder - {}'.format(name)) -app.add_route(handler, '/folder/') +app.add_route(handler2, '/folder/') -async def person_handler(request, name): +async def person_handler2(request, name): return text('Person - {}'.format(name)) -app.add_route(handler, '/person/') +app.add_route(person_handler2, '/person/') ``` From 0f6ed642daa5294e40bda11024c7c48950f5c4c8 Mon Sep 17 00:00:00 2001 From: Diogo Date: Fri, 30 Dec 2016 07:36:57 -0200 Subject: [PATCH 109/134] created methods to remove a route from api/router --- sanic/router.py | 21 +++++++++ sanic/sanic.py | 3 ++ tests/test_routes.py | 109 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 132 insertions(+), 1 deletion(-) diff --git a/sanic/router.py b/sanic/router.py index 4cc1f073..12f13240 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -23,6 +23,10 @@ class RouteExists(Exception): pass +class RouteDoesNotExist(Exception): + pass + + class Router: """ Router supports basic routing with parameters and method checks @@ -109,6 +113,23 @@ class Router: else: self.routes_static[uri] = route + def remove(self, uri, clean_cache=True): + try: + route = self.routes_all.pop(uri) + except KeyError: + raise RouteDoesNotExist("Route was not registered: {}".format(uri)) + + if route in self.routes_always_check: + self.routes_always_check.remove(route) + elif url_hash(uri) in self.routes_dynamic \ + and route in self.routes_dynamic[url_hash(uri)]: + self.routes_dynamic[url_hash(uri)].remove(route) + else: + self.routes_static.pop(uri) + + if clean_cache: + self._get.cache_clear() + def get(self, request): """ Gets a request handler based on the URL of the request, or raises an diff --git a/sanic/sanic.py b/sanic/sanic.py index ecf5b652..4a78a223 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -80,6 +80,9 @@ class Sanic: self.route(uri=uri, methods=methods)(handler) return handler + def remove_route(self, uri, clean_cache=True): + self.router.remove(uri, clean_cache) + # Decorator def exception(self, *exceptions): """ diff --git a/tests/test_routes.py b/tests/test_routes.py index 38591e53..149c71f9 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -2,7 +2,7 @@ import pytest from sanic import Sanic from sanic.response import text -from sanic.router import RouteExists +from sanic.router import RouteExists, RouteDoesNotExist from sanic.utils import sanic_endpoint_test @@ -356,3 +356,110 @@ def test_add_route_method_not_allowed(): request, response = sanic_endpoint_test(app, method='post', uri='/test') assert response.status == 405 + + +def test_remove_static_route(): + app = Sanic('test_remove_static_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.status == 200 + + request, response = sanic_endpoint_test(app, uri='/test2') + assert response.status == 200 + + app.remove_route('/test') + app.remove_route('/test2') + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 404 + + request, response = sanic_endpoint_test(app, uri='/test2') + assert response.status == 404 + + +def test_remove_dynamic_route(): + app = Sanic('test_remove_dynamic_route') + + async def handler(request, name): + return text('OK') + + app.add_route(handler, '/folder/') + + request, response = sanic_endpoint_test(app, uri='/folder/test123') + assert response.status == 200 + + app.remove_route('/folder/') + request, response = sanic_endpoint_test(app, uri='/folder/test123') + assert response.status == 404 + + +def test_remove_inexistent_route(): + app = Sanic('test_remove_inexistent_route') + + with pytest.raises(RouteDoesNotExist): + app.remove_route('/test') + + +def test_remove_unhashable_route(): + app = Sanic('test_remove_unhashable_route') + + 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 + + app.remove_route('/folder//end/') + + request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/') + assert response.status == 404 + + request, response = sanic_endpoint_test(app, uri='/folder/test///////end/') + assert response.status == 404 + + request, response = sanic_endpoint_test(app, uri='/folder/test/end/') + assert response.status == 404 + + +def test_remove_route_without_clean_cache(): + app = Sanic('test_remove_static_route') + + async def handler(request): + return text('OK') + + app.add_route(handler, '/test') + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 200 + + app.remove_route('/test', clean_cache=True) + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 404 + + app.add_route(handler, '/test') + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 200 + + app.remove_route('/test', clean_cache=False) + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 200 From 87559a34f8a06d401821beec3bc9f0d1dcb93d20 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 30 Dec 2016 12:13:16 -0600 Subject: [PATCH 110/134] Include more explicit loop for headers conversion Also merges master changes into this PR for this branch --- sanic/response.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 1202b101..f2eb02e5 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,9 +1,11 @@ from aiofiles import open as open_async -from .cookies import CookieJar, Cookie from mimetypes import guess_type from os import path + from ujson import dumps as json_dumps +from .cookies import CookieJar + COMMON_STATUS_CODES = { 200: b'OK', 400: b'Bad Request', @@ -79,7 +81,12 @@ class HTTPResponse: self.content_type = content_type if body is not None: - self.body = body.encode('utf-8') + try: + # Try to encode it regularly + self.body = body.encode('utf-8') + except AttributeError: + # Convert it to a str if you can't + self.body = str(body).encode('utf-8') else: self.body = body_bytes @@ -87,15 +94,6 @@ class HTTPResponse: self.headers = headers or {} self._cookies = None - @staticmethod - def format_header(name, value): - try: - return b'%b: %b\r\n' %\ - (name.encode(), value.encode('utf-8')) - except: - return b'%b: %b\r\n' %\ - (name.encode(), str(value).encode('utf-8')) - def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): # This is all returned in a kind-of funky way # We tried to make this as fast as possible in pure python @@ -105,10 +103,14 @@ class HTTPResponse: headers = b'' if self.headers: - headers = b''.join( - self.format_header(name, value) - for name, value in self.headers.items() - ) + for name, value in self.headers.items(): + try: + headers += ( + b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))) + except AttributeError: + headers += ( + b'%b: %b\r\n' % ( + str(name).encode(), str(value).encode('utf-8'))) # Try to pull from the common codes first # Speeds up response rate 6% over pulling from all From 7a8fd6b0df9567a019039b8f82ed1296b4307bf9 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 30 Dec 2016 13:48:17 -0600 Subject: [PATCH 111/134] Add more verbose error handling * Adds logging to error messages in debug mode as pointed out in PR #249, while also improving the debug message. --- sanic/exceptions.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 369a87a2..b9e6bf00 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -1,4 +1,5 @@ from .response import text +from .log import log from traceback import format_exc @@ -56,18 +57,31 @@ class Handler: :return: Response object """ handler = self.handlers.get(type(exception), self.default) - response = handler(request=request, exception=exception) + try: + response = handler(request=request, exception=exception) + except: + if self.sanic.debug: + response_message = ( + 'Exception raised in exception handler "{}" ' + 'for uri: "{}"\n{}').format( + handler.__name__, request.url, format_exc()) + log.error(response_message) + return text(response_message, 500) + else: + return text('An error occurred while handling an error', 500) return response def default(self, request, exception): if issubclass(type(exception), SanicException): return text( - "Error: {}".format(exception), + 'Error: {}'.format(exception), status=getattr(exception, 'status_code', 500)) elif self.sanic.debug: - return text( - "Error: {}\nException: {}".format( - exception, format_exc()), status=500) + response_message = ( + 'Exception occurred while handling uri: "{}"\n{}'.format( + request.url, format_exc())) + log.error(response_message) + return text(response_message, status=500) else: return text( - "An error occurred while generating the request", status=500) + 'An error occurred while generating the response', status=500) From 15c965c08c68807b38121a9ab229214949ffd592 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 30 Dec 2016 13:50:12 -0600 Subject: [PATCH 112/134] Make exception tests test unhandled exceptions * Adds tests for unhandled exceptions * Adds tests for unhandled exceptions in exception handlers * Rewrites tests to utilize pytest fixtures (No need to create the app on import) --- tests/test_exceptions.py | 87 ++++++++++++++++++++++++++++------------ 1 file changed, 61 insertions(+), 26 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 28e766cd..5cebfb87 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,51 +1,86 @@ +import pytest + from sanic import Sanic from sanic.response import text from sanic.exceptions import InvalidUsage, ServerError, NotFound from sanic.utils import sanic_endpoint_test -# ------------------------------------------------------------ # -# GET -# ------------------------------------------------------------ # -exception_app = Sanic('test_exceptions') +class SanicExceptionTestException(Exception): + pass -@exception_app.route('/') -def handler(request): - return text('OK') +@pytest.fixture(scope='module') +def exception_app(): + app = Sanic('test_exceptions') + + @app.route('/') + def handler(request): + return text('OK') + + @app.route('/error') + def handler_error(request): + raise ServerError("OK") + + @app.route('/404') + def handler_404(request): + raise NotFound("OK") + + @app.route('/invalid') + def handler_invalid(request): + raise InvalidUsage("OK") + + @app.route('/divide_by_zero') + def handle_unhandled_exception(request): + 1 / 0 + + @app.route('/error_in_error_handler_handler') + def custom_error_handler(request): + raise SanicExceptionTestException('Dummy message!') + + @app.exception(SanicExceptionTestException) + def error_in_error_handler_handler(request, exception): + 1 / 0 + + return app -@exception_app.route('/error') -def handler_error(request): - raise ServerError("OK") - - -@exception_app.route('/404') -def handler_404(request): - raise NotFound("OK") - - -@exception_app.route('/invalid') -def handler_invalid(request): - raise InvalidUsage("OK") - - -def test_no_exception(): +def test_no_exception(exception_app): + """Test that a route works without an exception""" request, response = sanic_endpoint_test(exception_app) assert response.status == 200 assert response.text == 'OK' -def test_server_error_exception(): +def test_server_error_exception(exception_app): + """Test the built-in ServerError exception works""" request, response = sanic_endpoint_test(exception_app, uri='/error') assert response.status == 500 -def test_invalid_usage_exception(): +def test_invalid_usage_exception(exception_app): + """Test the built-in InvalidUsage exception works""" request, response = sanic_endpoint_test(exception_app, uri='/invalid') assert response.status == 400 -def test_not_found_exception(): +def test_not_found_exception(exception_app): + """Test the built-in NotFound exception works""" request, response = sanic_endpoint_test(exception_app, uri='/404') assert response.status == 404 + + +def test_handled_unhandled_exception(exception_app): + """Test that an exception not built into sanic is handled""" + request, response = sanic_endpoint_test( + exception_app, uri='/divide_by_zero') + assert response.status == 500 + assert response.body == b'An error occurred while generating the response' + + +def test_exception_in_exception_handler(exception_app): + """Test that an exception thrown in an error handler is handled""" + request, response = sanic_endpoint_test( + exception_app, uri='/error_in_error_handler_handler') + assert response.status == 500 + assert response.body == b'An error occurred while handling an error' From 87c24e5a7cd629c25f100e77b35bbebb1b9fe210 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Mon, 2 Jan 2017 11:57:51 +0900 Subject: [PATCH 113/134] Prevent flooding of meaningless traceback in `sanic_endpoint_test` When Sanic has an exception in a request middleware, it fails to save request object in `results`. In `sanic_endpoint_test`, because it always requires `results` to have both `request` and `response` objects, it prints traceback like attached example. It is not a user code and it doesn't give any information to users, it is better to suppress to print this kind of error. To fix it, this patch insert collect hook as first request middleware to guarantee to successfully run it always. ``` app = , method = 'get', uri = '/ping/', gather_request = True, loop = None debug = True, request_args = (), request_kwargs = {} _collect_request = ._collect_request at 0x11286c158> _collect_response = ._collect_response at 0x11286c378> def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, loop=None, debug=False, *request_args, **request_kwargs): results = [] exceptions = [] if gather_request: @app.middleware def _collect_request(request): results.append(request) async def _collect_response(sanic, loop): try: response = await local_request(method, uri, *request_args, **request_kwargs) results.append(response) except Exception as e: exceptions.append(e) app.stop() app.run(host=HOST, debug=debug, port=42101, after_start=_collect_response, loop=loop) if exceptions: raise ValueError("Exception during request: {}".format(exceptions)) if gather_request: try: > request, response = results E ValueError: not enough values to unpack (expected 2, got 1) ../sanic/sanic/utils.py:46: ValueError ``` --- sanic/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/utils.py b/sanic/utils.py index 88444b3c..cb6d70c4 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -22,9 +22,9 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, exceptions = [] if gather_request: - @app.middleware def _collect_request(request): results.append(request) + app.request_middleware.appendleft(_collect_request) async def _collect_response(sanic, loop): try: From 31e92a8b4f371141d5e3f2eec0d7971796a75b7e Mon Sep 17 00:00:00 2001 From: Hyunjun Kim Date: Mon, 2 Jan 2017 13:32:14 +0900 Subject: [PATCH 114/134] Update .gitignore * .python-version is generated by `pyenv local` command * .eggs/ directory contains *.egg files --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d7872c5c..7fb5634f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,13 @@ *~ *.egg-info *.egg +*.eggs +*.pyc .coverage .coverage.* coverage .tox settings.py -*.pyc .idea/* .cache/* +.python-version From 035cbf84ae4454422b6dbd1861401404963f7e6b Mon Sep 17 00:00:00 2001 From: Hyunjun Kim Date: Mon, 2 Jan 2017 14:20:20 +0900 Subject: [PATCH 115/134] Cache request.json even when it's null or empty In case of request body is set to `{}`, `[]` or `null`, even it's already processed, parsed_json won't be used due to its boolean evaluation. --- sanic/request.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 4309bbed..a9f0364d 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -56,7 +56,7 @@ class Request(dict): # Init but do not inhale self.body = None - self.parsed_json = None + self.parsed_json = ... self.parsed_form = None self.parsed_files = None self.parsed_args = None @@ -64,7 +64,7 @@ class Request(dict): @property def json(self): - if not self.parsed_json: + if self.parsed_json is ...: try: self.parsed_json = json_loads(self.body) except Exception: From cfdd9f66d1a7eae4d5fe091c1499f5c77d735ccd Mon Sep 17 00:00:00 2001 From: Hyunjun Kim Date: Mon, 2 Jan 2017 13:29:31 +0900 Subject: [PATCH 116/134] Correct sanic.router.Router documentation --- sanic/router.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sanic/router.py b/sanic/router.py index 12f13240..081b5da6 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -31,12 +31,12 @@ class Router: """ Router supports basic routing with parameters and method checks Usage: - @sanic.route('/my/url/', methods=['GET', 'POST', ...]) - def my_route(request, my_parameter): + @app.route('/my_url/', methods=['GET', 'POST', ...]) + def my_route(request, my_param): do stuff... or - @sanic.route('/my/url/:type', methods['GET', 'POST', ...]) - def my_route_with_type(request, my_parameter): + @app.route('/my_url/', methods=['GET', 'POST', ...]) + def my_route_with_type(request, my_param: my_type): do stuff... Parameters will be passed as keyword arguments to the request handling From e6eb697bb2ba8840f8a92746426abb2f213a15d1 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 4 Jan 2017 05:40:13 +0900 Subject: [PATCH 117/134] Use constant PORT rather than literal in test code (#266) --- sanic/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/utils.py b/sanic/utils.py index 9f4a97d8..47214872 100644 --- a/sanic/utils.py +++ b/sanic/utils.py @@ -35,7 +35,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, exceptions.append(e) app.stop() - app.run(host=HOST, debug=debug, port=42101, + app.run(host=HOST, debug=debug, port=PORT, after_start=_collect_response, loop=loop, **server_kwargs) if exceptions: From e7922c1b547d58e605cf5a877ace8214c992d987 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 3 Jan 2017 18:35:11 -0800 Subject: [PATCH 118/134] add configurable backlog #263 --- sanic/sanic.py | 6 ++++-- sanic/server.py | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 22ed234e..d0674360 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -20,7 +20,7 @@ from .exceptions import ServerError class Sanic: def __init__(self, name=None, router=None, - error_handler=None, logger=None): + error_handler=None, logger=None, backlog=100): if logger is None: logging.basicConfig( level=logging.INFO, @@ -29,6 +29,7 @@ class Sanic: if name is None: frame_records = stack()[1] name = getmodulename(frame_records[1]) + self.backlog = backlog self.name = name self.router = router or Router() self.error_handler = error_handler or Handler(self) @@ -278,7 +279,8 @@ class Sanic: 'error_handler': self.error_handler, 'request_timeout': self.config.REQUEST_TIMEOUT, 'request_max_size': self.config.REQUEST_MAX_SIZE, - 'loop': loop + 'loop': loop, + 'backlog': self.backlog } # -------------------------------------------- # diff --git a/sanic/server.py b/sanic/server.py index e789f173..ec207d26 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -226,7 +226,7 @@ def trigger_events(events, loop): 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, protocol=HttpProtocol): + reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100): """ Starts asynchronous HTTP Server on an individual process. :param host: Address to host on @@ -276,7 +276,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, host, port, reuse_port=reuse_port, - sock=sock + sock=sock, + backlog=backlog ) # Instead of pulling time at the end of every request, From 06911a8d2e4fdec7b811c5d2a6fc86b309586086 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Wed, 4 Jan 2017 00:23:35 -0600 Subject: [PATCH 119/134] Add tests for server start/stop event functions --- tests/tests_server_events.py | 59 ++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/tests_server_events.py diff --git a/tests/tests_server_events.py b/tests/tests_server_events.py new file mode 100644 index 00000000..27a5af29 --- /dev/null +++ b/tests/tests_server_events.py @@ -0,0 +1,59 @@ +from io import StringIO +from random import choice +from string import ascii_letters +import signal + +import pytest + +from sanic import Sanic + +AVAILABLE_LISTENERS = [ + 'before_start', + 'after_start', + 'before_stop', + 'after_stop' +] + + +def create_listener(listener_name, in_list): + async def _listener(app, loop): + print('DEBUG MESSAGE FOR PYTEST for {}'.format(listener_name)) + in_list.insert(0, app.name + listener_name) + return _listener + + +def start_stop_app(random_name_app, **run_kwargs): + + def stop_on_alarm(signum, frame): + raise KeyboardInterrupt('SIGINT for sanic to stop gracefully') + + signal.signal(signal.SIGALRM, stop_on_alarm) + signal.alarm(1) + try: + random_name_app.run(**run_kwargs) + except KeyboardInterrupt: + pass + + +@pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS) +def test_single_listener(listener_name): + """Test that listeners on their own work""" + random_name_app = Sanic(''.join( + [choice(ascii_letters) for _ in range(choice(range(5, 10)))])) + output = list() + start_stop_app( + random_name_app, + **{listener_name: create_listener(listener_name, output)}) + assert random_name_app.name + listener_name == output.pop() + + +def test_all_listeners(): + random_name_app = Sanic(''.join( + [choice(ascii_letters) for _ in range(choice(range(5, 10)))])) + output = list() + start_stop_app( + random_name_app, + **{listener_name: create_listener(listener_name, output) + for listener_name in AVAILABLE_LISTENERS}) + for listener_name in AVAILABLE_LISTENERS: + assert random_name_app.name + listener_name == output.pop() From 9c91b09ab1425eb486a19abaf94da63173e109cb Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Wed, 4 Jan 2017 00:23:59 -0600 Subject: [PATCH 120/134] Fix this to actually reflect current behavior --- examples/try_everything.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/try_everything.py b/examples/try_everything.py index 80358ddb..f386fb03 100644 --- a/examples/try_everything.py +++ b/examples/try_everything.py @@ -64,11 +64,11 @@ def query_string(request): # Run Server # ----------------------------------------------- # -def after_start(loop): +def after_start(app, loop): log.info("OH OH OH OH OHHHHHHHH") -def before_stop(loop): +def before_stop(app, loop): log.info("TRIED EVERYTHING") From b67482de9b5272bf88aef59ac3c685f52fbf5771 Mon Sep 17 00:00:00 2001 From: DanielChien Date: Wed, 4 Jan 2017 23:29:09 +0800 Subject: [PATCH 121/134] add example for asyncpg --- examples/sanic_asyncpg_example.py | 64 +++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 examples/sanic_asyncpg_example.py diff --git a/examples/sanic_asyncpg_example.py b/examples/sanic_asyncpg_example.py new file mode 100644 index 00000000..01ac1984 --- /dev/null +++ b/examples/sanic_asyncpg_example.py @@ -0,0 +1,64 @@ +""" To run this example you need additional asyncpg package + +""" +import os +import asyncio + +import uvloop +from asyncpg import create_pool +import sqlalchemy as sa + +from sanic import Sanic +from sanic.response import json + +asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + +DB_CONFIG = { + 'host': 'localhost', + 'user': 'tachien', + 'database': 'tachien' +} + +def jsonify(records): + """ Parse asyncpg record response into JSON format + + """ + return [{key: value for key, value in + zip(r.keys(), r.values())} for r in records] + +loop = asyncio.get_event_loop() + +async def make_pool(): + return await create_pool(**DB_CONFIG) + +app = Sanic(__name__) +pool = loop.run_until_complete(make_pool()) + +async def create_db(): + """ Create some table and add some data + + """ + async with pool.acquire() as connection: + async with connection.transaction(): + await connection.execute('DROP TABLE IF EXISTS sanic_post') + await connection.execute("""CREATE TABLE sanic_post ( + id serial primary key, + content varchar(50), + post_date timestamp + );""") + for i in range(0, 100): + await connection.execute(f"""INSERT INTO sanic_post + (id, content, post_date) VALUES ({i}, {i}, now())""") + + +@app.route("/") +async def handler(request): + async with pool.acquire() as connection: + async with connection.transaction(): + results = await connection.fetch('SELECT * FROM sanic_post') + return json({'posts': jsonify(results)}) + + +if __name__ == '__main__': + loop.run_until_complete(create_db()) + app.run(host='0.0.0.0', port=8000, loop=loop) From 19426444344d428ac0a59c676f5ecd4aff5a35ae Mon Sep 17 00:00:00 2001 From: DanielChien Date: Wed, 4 Jan 2017 23:30:29 +0800 Subject: [PATCH 122/134] modify config to varbles --- examples/sanic_asyncpg_example.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/sanic_asyncpg_example.py b/examples/sanic_asyncpg_example.py index 01ac1984..9817ff57 100644 --- a/examples/sanic_asyncpg_example.py +++ b/examples/sanic_asyncpg_example.py @@ -14,9 +14,11 @@ from sanic.response import json asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) DB_CONFIG = { - 'host': 'localhost', - 'user': 'tachien', - 'database': 'tachien' + 'host': '', + 'user': '', + 'password': '', + 'port': '', + 'database': '' } def jsonify(records): From 5c7c2cf85e1f742b1793ec7c20f167197e917883 Mon Sep 17 00:00:00 2001 From: easydaniel Date: Wed, 4 Jan 2017 23:35:06 +0800 Subject: [PATCH 123/134] Update sanic_asyncpg_example.py Remove unused library --- examples/sanic_asyncpg_example.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/sanic_asyncpg_example.py b/examples/sanic_asyncpg_example.py index 9817ff57..142480e1 100644 --- a/examples/sanic_asyncpg_example.py +++ b/examples/sanic_asyncpg_example.py @@ -6,7 +6,6 @@ import asyncio import uvloop from asyncpg import create_pool -import sqlalchemy as sa from sanic import Sanic from sanic.response import json From 616e20d4674de93f89b3f0b509e206c31ae9a2bc Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Wed, 4 Jan 2017 09:31:06 -0800 Subject: [PATCH 124/134] move backlog to run() --- sanic/sanic.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index d0674360..a3f49197 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -20,7 +20,7 @@ from .exceptions import ServerError class Sanic: def __init__(self, name=None, router=None, - error_handler=None, logger=None, backlog=100): + error_handler=None, logger=None): if logger is None: logging.basicConfig( level=logging.INFO, @@ -29,7 +29,6 @@ class Sanic: if name is None: frame_records = stack()[1] name = getmodulename(frame_records[1]) - self.backlog = backlog self.name = name self.router = router or Router() self.error_handler = error_handler or Handler(self) @@ -243,7 +242,7 @@ class Sanic: def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, after_start=None, before_stop=None, after_stop=None, sock=None, - workers=1, loop=None, protocol=HttpProtocol): + workers=1, loop=None, protocol=HttpProtocol, backlog=100): """ Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drains connections before closing. @@ -280,7 +279,7 @@ class Sanic: 'request_timeout': self.config.REQUEST_TIMEOUT, 'request_max_size': self.config.REQUEST_MAX_SIZE, 'loop': loop, - 'backlog': self.backlog + 'backlog': backlog } # -------------------------------------------- # From baf8254907720cb35212f00afa48787fa8558b6b Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Thu, 5 Jan 2017 15:29:57 -0600 Subject: [PATCH 125/134] Change Ellipsis to None for consistency --- sanic/request.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index a9f0364d..7ca5523c 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -56,7 +56,7 @@ class Request(dict): # Init but do not inhale self.body = None - self.parsed_json = ... + self.parsed_json = None self.parsed_form = None self.parsed_files = None self.parsed_args = None @@ -64,7 +64,7 @@ class Request(dict): @property def json(self): - if self.parsed_json is ...: + if self.parsed_json is None: try: self.parsed_json = json_loads(self.body) except Exception: From fcae4a9f0a5217b3b5ea6495821a85773aa33c55 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sat, 7 Jan 2017 06:30:23 +0200 Subject: [PATCH 126/134] added as_view --- sanic/views.py | 25 ++++++++++++++++++++++++- tests/test_views.py | 14 +++++++------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/sanic/views.py b/sanic/views.py index 9387bcf6..45a09ef1 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -28,12 +28,35 @@ class HTTPMethodView: To add the view into the routing you could use 1) app.add_route(DummyView(), '/') 2) app.route('/')(DummyView()) + + TODO: add doc about devorators """ - def __call__(self, request, *args, **kwargs): + decorators = () + + 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): + """ TODO: add docs + + """ + def view(*args, **kwargs): + self = view.view_class(*class_args, **class_kwargs) + return self.dispatch_request(*args, **kwargs) + + if cls.decorators: + view.__module__ = cls.__module__ + for decorator in cls.decorators: + view = decorator(view) + + view.view_class = cls + view.__doc__ = cls.__doc__ + view.__module__ = cls.__module__ + return view diff --git a/tests/test_views.py b/tests/test_views.py index 59acb847..af38277e 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -26,7 +26,7 @@ def test_methods(): def delete(self, request): return text('I am delete method') - app.add_route(DummyView(), '/') + app.add_route(DummyView.as_view(), '/') request, response = sanic_endpoint_test(app, method="get") assert response.text == 'I am get method' @@ -48,7 +48,7 @@ def test_unexisting_methods(): def get(self, request): return text('I am get method') - app.add_route(DummyView(), '/') + app.add_route(DummyView.as_view(), '/') request, response = sanic_endpoint_test(app, method="get") assert response.text == 'I am get method' request, response = sanic_endpoint_test(app, method="post") @@ -63,7 +63,7 @@ def test_argument_methods(): def get(self, request, my_param_here): return text('I am get method with %s' % my_param_here) - app.add_route(DummyView(), '/') + app.add_route(DummyView.as_view(), '/') request, response = sanic_endpoint_test(app, uri='/test123') @@ -79,7 +79,7 @@ def test_with_bp(): def get(self, request): return text('I am get method') - bp.add_route(DummyView(), '/') + bp.add_route(DummyView.as_view(), '/') app.blueprint(bp) request, response = sanic_endpoint_test(app) @@ -96,7 +96,7 @@ def test_with_bp_with_url_prefix(): def get(self, request): return text('I am get method') - bp.add_route(DummyView(), '/') + bp.add_route(DummyView.as_view(), '/') app.blueprint(bp) request, response = sanic_endpoint_test(app, uri='/test1/') @@ -112,7 +112,7 @@ def test_with_middleware(): def get(self, request): return text('I am get method') - app.add_route(DummyView(), '/') + app.add_route(DummyView.as_view(), '/') results = [] @@ -145,7 +145,7 @@ def test_with_middleware_response(): def get(self, request): return text('I am get method') - app.add_route(DummyView(), '/') + app.add_route(DummyView.as_view(), '/') request, response = sanic_endpoint_test(app) From 1317b1799ccf50f01d30b71c4b6c21a1c31dfc29 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sat, 7 Jan 2017 06:57:07 +0200 Subject: [PATCH 127/134] add docstrings&updated docs --- docs/class_based_views.md | 17 +++++++++++++++-- sanic/views.py | 13 +++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/docs/class_based_views.md b/docs/class_based_views.md index ee410b1d..27972e82 100644 --- a/docs/class_based_views.md +++ b/docs/class_based_views.md @@ -28,7 +28,7 @@ class SimpleView(HTTPMethodView): def delete(self, request): return text('I am delete method') -app.add_route(SimpleView(), '/') +app.add_route(SimpleView.as_view(), '/') ``` @@ -40,6 +40,19 @@ class NameView(HTTPMethodView): def get(self, request, name): return text('Hello {}'.format(name)) -app.add_route(NameView(), '/') +app.add_route(NameView.as_view(), '/') + +``` + +If you want to add decorator for class, you could set decorators variable + +``` +class ViewWithDecorator(HTTPMethodView): + decorators = (some_decorator_here) + + def get(self, request, name): + return text('Hello I have a decorator') + +app.add_route(ViewWithDecorator.as_view(), '/url') ``` diff --git a/sanic/views.py b/sanic/views.py index 45a09ef1..eeaa8d38 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -7,7 +7,7 @@ class HTTPMethodView: to every HTTP method you want to support. For example: - class DummyView(View): + class DummyView(HTTPMethodView): def get(self, request, *args, **kwargs): return text('I am get method') @@ -20,16 +20,16 @@ class HTTPMethodView: 405 response. If you need any url params just mention them in method definition: - class DummyView(View): + class DummyView(HTTPMethodView): 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()) + 1) app.add_route(DummyView.as_view(), '/') + 2) app.route('/')(DummyView.as_view()) - TODO: add doc about devorators + To add any decorator you could set it into decorators variable """ decorators = () @@ -44,7 +44,8 @@ class HTTPMethodView: @classmethod def as_view(cls, *class_args, **class_kwargs): - """ TODO: add docs + """ Converts the class into an actual view function that can be used + with the routing system. """ def view(*args, **kwargs): From 47a4f34cdff4417a746f56ced29b698be7550fce Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sat, 7 Jan 2017 07:13:49 +0200 Subject: [PATCH 128/134] tests&small update --- docs/class_based_views.md | 2 +- sanic/views.py | 2 +- tests/test_views.py | 41 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/docs/class_based_views.md b/docs/class_based_views.md index 27972e82..84a5b952 100644 --- a/docs/class_based_views.md +++ b/docs/class_based_views.md @@ -48,7 +48,7 @@ If you want to add decorator for class, you could set decorators variable ``` class ViewWithDecorator(HTTPMethodView): - decorators = (some_decorator_here) + decorators = [some_decorator_here] def get(self, request, name): return text('Hello I have a decorator') diff --git a/sanic/views.py b/sanic/views.py index eeaa8d38..0222b96f 100644 --- a/sanic/views.py +++ b/sanic/views.py @@ -32,7 +32,7 @@ class HTTPMethodView: To add any decorator you could set it into decorators variable """ - decorators = () + decorators = [] def dispatch_request(self, request, *args, **kwargs): handler = getattr(self, request.method.lower(), None) diff --git a/tests/test_views.py b/tests/test_views.py index af38277e..9447ab61 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -153,3 +153,44 @@ def test_with_middleware_response(): assert type(results[0]) is Request assert type(results[1]) is Request assert issubclass(type(results[2]), HTTPResponse) + + +def test_with_custom_class_methods(): + app = Sanic('test_with_custom_class_methods') + + class DummyView(HTTPMethodView): + global_var = 0 + + def _iternal_method(self): + self.global_var += 10 + + def get(self, request): + self._iternal_method() + return text('I am get method and global var is {}'.format(self.global_var)) + + app.add_route(DummyView.as_view(), '/') + request, response = sanic_endpoint_test(app, method="get") + assert response.text == 'I am get method and global var is 10' + + +def test_with_decorator(): + app = Sanic('test_with_decorator') + + results = [] + + def stupid_decorator(view): + def decorator(*args, **kwargs): + results.append(1) + return view(*args, **kwargs) + return decorator + + class DummyView(HTTPMethodView): + decorators = [stupid_decorator] + + def get(self, request): + return text('I am get method') + + app.add_route(DummyView.as_view(), '/') + request, response = sanic_endpoint_test(app, method="get", debug=True) + assert response.text == 'I am get method' + assert results[0] == 1 From 434fa74e67045ec397b5d7b5fdbdf2e25b9163a6 Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Sat, 7 Jan 2017 07:14:27 +0200 Subject: [PATCH 129/134] removed debug from test --- tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_views.py b/tests/test_views.py index 9447ab61..592893a4 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -191,6 +191,6 @@ def test_with_decorator(): return text('I am get method') app.add_route(DummyView.as_view(), '/') - request, response = sanic_endpoint_test(app, method="get", debug=True) + request, response = sanic_endpoint_test(app, method="get") assert response.text == 'I am get method' assert results[0] == 1 From 77c04c4cf9ac5d5a84de025713423bf766f577bf Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Fri, 6 Jan 2017 18:32:30 -0800 Subject: [PATCH 130/134] fix multiple worker problem --- sanic/sanic.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index a3f49197..e5873236 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -16,6 +16,8 @@ from .router import Router from .server import serve, HttpProtocol from .static import register as static_register from .exceptions import ServerError +from socket import socket +from os import set_inheritable class Sanic: @@ -350,19 +352,19 @@ class Sanic: signal(SIGINT, lambda s, f: stop_event.set()) signal(SIGTERM, lambda s, f: stop_event.set()) + sock = socket() + sock.bind((server_settings['host'], server_settings['port'])) + set_inheritable(sock.fileno(), True) + server_settings['sock'] = sock + server_settings['host'] = None + server_settings['port'] = None + processes = [] for _ in range(workers): process = Process(target=serve, kwargs=server_settings) process.start() processes.append(process) - # Infinitely wait for the stop event - try: - select(stop_event) - except: - pass - - log.info('Spinning down workers...') for process in processes: process.terminate() for process in processes: From ed8e3f237cd94f2a0406ad9db2fb99f556640022 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 7 Jan 2017 15:28:21 -0800 Subject: [PATCH 131/134] this branch is broken --- sanic/sanic.py | 12 ++++++++---- tests/test_multiprocessing.py | 37 +++++++++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index e5873236..6d855fc2 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -16,7 +16,7 @@ from .router import Router from .server import serve, HttpProtocol from .static import register as static_register from .exceptions import ServerError -from socket import socket +from socket import socket, SOL_SOCKET, SO_REUSEADDR from os import set_inheritable @@ -244,7 +244,8 @@ class Sanic: def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, after_start=None, before_stop=None, after_stop=None, sock=None, - workers=1, loop=None, protocol=HttpProtocol, backlog=100): + workers=1, loop=None, protocol=HttpProtocol, backlog=100, + stop_event=None): """ Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drains connections before closing. @@ -320,7 +321,7 @@ class Sanic: else: log.info('Spinning up {} workers...'.format(workers)) - self.serve_multiple(server_settings, workers) + self.serve_multiple(server_settings, workers, stop_event) except Exception as e: log.exception( @@ -335,7 +336,7 @@ class Sanic: get_event_loop().stop() @staticmethod - def serve_multiple(server_settings, workers, stop_event=None): + def serve_multiple(self, server_settings, workers, stop_event=None): """ Starts multiple server processes simultaneously. Stops on interrupt and terminate signals, and drains connections when complete. @@ -353,6 +354,7 @@ class Sanic: signal(SIGTERM, lambda s, f: stop_event.set()) sock = socket() + #sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) sock.bind((server_settings['host'], server_settings['port'])) set_inheritable(sock.fileno(), True) server_settings['sock'] = sock @@ -362,10 +364,12 @@ class Sanic: processes = [] for _ in range(workers): process = Process(target=serve, kwargs=server_settings) + process.daemon = True process.start() processes.append(process) for process in processes: process.terminate() + for process in processes: process.join() diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index cc967ef1..52a68fd1 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -1,9 +1,13 @@ from multiprocessing import Array, Event, Process from time import sleep, time from ujson import loads as json_loads +from asyncio import get_event_loop +from os import killpg, kill +from signal import SIGUSR1, signal, SIGINT, SIGTERM, SIGKILL from sanic import Sanic -from sanic.response import json +from sanic.response import json, text +from sanic.exceptions import Handler from sanic.utils import local_request, HOST, PORT @@ -50,11 +54,12 @@ def skip_test_multiprocessing(): except: raise ValueError("Expected JSON response but got '{}'".format(response)) + stop_event.set() assert results.get('test') == True def test_drain_connections(): - app = Sanic('test_json') + app = Sanic('test_stop') @app.route('/') async def handler(request): @@ -75,3 +80,31 @@ def test_drain_connections(): end = time() assert end - start < 0.05 + +def skip_test_workers(): + app = Sanic('test_workers') + + @app.route('/') + async def handler(request): + return text('ok') + + stop_event = Event() + + d = [] + async def after_start(*args, **kwargs): + http_response = await local_request('get', '/') + d.append(http_response.text) + stop_event.set() + + p = Process(target=app.run, kwargs={'host':HOST, + 'port':PORT, + 'after_start': after_start, + 'workers':2, + 'stop_event':stop_event}) + p.start() + loop = get_event_loop() + loop.run_until_complete(after_start()) + #killpg(p.pid, SIGUSR1) + kill(p.pid, SIGUSR1) + + assert d[0] == 1 From dd28d70680e79a918d069313351c1a79297fcc2f Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 7 Jan 2017 15:46:43 -0800 Subject: [PATCH 132/134] fix stop event --- sanic/sanic.py | 29 ++++++++++++++------------- tests/test_multiprocessing.py | 37 ++--------------------------------- 2 files changed, 17 insertions(+), 49 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 6d855fc2..b3487b53 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -3,7 +3,6 @@ from collections import deque from functools import partial from inspect import isawaitable, stack, getmodulename from multiprocessing import Process, Event -from select import select from signal import signal, SIGTERM, SIGINT from traceback import format_exc import logging @@ -41,6 +40,8 @@ class Sanic: self._blueprint_order = [] self.loop = None self.debug = None + self.sock = None + self.processes = None # Register alternative method names self.go_fast = self.run @@ -333,9 +334,12 @@ class Sanic: """ This kills the Sanic """ + if self.processes is not None: + for process in self.processes: + process.terminate() + self.sock.close() get_event_loop().stop() - @staticmethod def serve_multiple(self, server_settings, workers, stop_event=None): """ Starts multiple server processes simultaneously. Stops on interrupt @@ -348,28 +352,25 @@ class Sanic: server_settings['reuse_port'] = True # Create a stop event to be triggered by a signal - if not stop_event: + if stop_event is None: stop_event = Event() signal(SIGINT, lambda s, f: stop_event.set()) signal(SIGTERM, lambda s, f: stop_event.set()) - sock = socket() - #sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) - sock.bind((server_settings['host'], server_settings['port'])) - set_inheritable(sock.fileno(), True) - server_settings['sock'] = sock + self.sock = socket() + self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) + self.sock.bind((server_settings['host'], server_settings['port'])) + set_inheritable(self.sock.fileno(), True) + server_settings['sock'] = self.sock server_settings['host'] = None server_settings['port'] = None - processes = [] + self.processes = [] for _ in range(workers): process = Process(target=serve, kwargs=server_settings) process.daemon = True process.start() - processes.append(process) + self.processes.append(process) - for process in processes: - process.terminate() - - for process in processes: + for process in self.processes: process.join() diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index 52a68fd1..cc967ef1 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -1,13 +1,9 @@ from multiprocessing import Array, Event, Process from time import sleep, time from ujson import loads as json_loads -from asyncio import get_event_loop -from os import killpg, kill -from signal import SIGUSR1, signal, SIGINT, SIGTERM, SIGKILL from sanic import Sanic -from sanic.response import json, text -from sanic.exceptions import Handler +from sanic.response import json from sanic.utils import local_request, HOST, PORT @@ -54,12 +50,11 @@ def skip_test_multiprocessing(): except: raise ValueError("Expected JSON response but got '{}'".format(response)) - stop_event.set() assert results.get('test') == True def test_drain_connections(): - app = Sanic('test_stop') + app = Sanic('test_json') @app.route('/') async def handler(request): @@ -80,31 +75,3 @@ def test_drain_connections(): end = time() assert end - start < 0.05 - -def skip_test_workers(): - app = Sanic('test_workers') - - @app.route('/') - async def handler(request): - return text('ok') - - stop_event = Event() - - d = [] - async def after_start(*args, **kwargs): - http_response = await local_request('get', '/') - d.append(http_response.text) - stop_event.set() - - p = Process(target=app.run, kwargs={'host':HOST, - 'port':PORT, - 'after_start': after_start, - 'workers':2, - 'stop_event':stop_event}) - p.start() - loop = get_event_loop() - loop.run_until_complete(after_start()) - #killpg(p.pid, SIGUSR1) - kill(p.pid, SIGUSR1) - - assert d[0] == 1 From f8e6becb9e694a11b7ea6b049453399f2235daa8 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sat, 7 Jan 2017 18:58:02 -0800 Subject: [PATCH 133/134] skip multiprocessing tests --- sanic/sanic.py | 3 +++ tests/test_multiprocessing.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index b3487b53..3dab3e47 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -374,3 +374,6 @@ class Sanic: for process in self.processes: process.join() + + # the above processes will block this until they're stopped + self.stop() diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index cc967ef1..7a5fd1c9 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -53,7 +53,7 @@ def skip_test_multiprocessing(): assert results.get('test') == True -def test_drain_connections(): +def skip_test_drain_connections(): app = Sanic('test_json') @app.route('/') From 5566668a5f6aeab044a5f1b2f2394b63fcdfa554 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sun, 8 Jan 2017 11:55:08 -0600 Subject: [PATCH 134/134] Change the skips to actual pytest skips By using the builtin pytest skips we can identify that the tests are still there but are being currently skipped. Will update later to remove the skips once we figure out why they freeze with pytest (I experienced this same issue with multiprocessing when testing start/stop events). --- tests/test_multiprocessing.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index 7a5fd1c9..e39c3d24 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -2,6 +2,8 @@ from multiprocessing import Array, Event, Process from time import sleep, time from ujson import loads as json_loads +import pytest + from sanic import Sanic from sanic.response import json from sanic.utils import local_request, HOST, PORT @@ -13,8 +15,9 @@ from sanic.utils import local_request, HOST, PORT # TODO: Figure out why this freezes on pytest but not when # executed via interpreter - -def skip_test_multiprocessing(): +@pytest.mark.skip( + reason="Freezes with pytest not on interpreter") +def test_multiprocessing(): app = Sanic('test_json') response = Array('c', 50) @@ -52,8 +55,9 @@ def skip_test_multiprocessing(): assert results.get('test') == True - -def skip_test_drain_connections(): +@pytest.mark.skip( + reason="Freezes with pytest not on interpreter") +def test_drain_connections(): app = Sanic('test_json') @app.route('/')