From c657c531b482039c18bcf926052fcc1f6c8377d0 Mon Sep 17 00:00:00 2001 From: 38elements Date: Fri, 23 Dec 2016 00:13:38 +0900 Subject: [PATCH 01/71] 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 32ea45d403ddb1e1b143ba2fbae0675f32adbc50 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Thu, 22 Dec 2016 21:00:57 -0800 Subject: [PATCH 02/71] 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 03/71] 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 2d05243c4a2a6d10a868cacddc103bbd25921d0b Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 24 Dec 2016 22:49:48 +0900 Subject: [PATCH 04/71] 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 7e6c92dc52525cdf30babc9f40125cddafab8b43 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 13 Dec 2016 21:24:26 -0800 Subject: [PATCH 05/71] 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 06/71] 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 07/71] 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 08/71] 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 09/71] 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 10/71] 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 11/71] 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 12/71] 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 13/71] 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 14/71] 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 15/71] 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 16/71] 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 17/71] 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 18/71] 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 19/71] 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 20/71] 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 21/71] 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 22/71] 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 23/71] 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 24/71] 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 25/71] 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 26/71] 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 27/71] 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 28/71] 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 29/71] 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 30/71] 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 31/71] 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 32/71] 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 33/71] 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 34/71] 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 35/71] 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 36/71] 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 37/71] 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 38/71] 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 39/71] 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 40/71] 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 41/71] 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 42/71] 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 43/71] 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 44/71] 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 45/71] 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 46/71] 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 47/71] 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('/') From 4f832ac9afdba11f276b42ccbfbf7aa83c60dc47 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Sun, 8 Jan 2017 15:48:12 -0800 Subject: [PATCH 48/71] add support for virtual hosts --- examples/vhosts.py | 18 ++++++++++++++++++ sanic/router.py | 27 +++++++++++++++++++++++---- sanic/sanic.py | 13 +++++++------ tests/test_vhosts.py | 23 +++++++++++++++++++++++ 4 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 examples/vhosts.py create mode 100644 tests/test_vhosts.py diff --git a/examples/vhosts.py b/examples/vhosts.py new file mode 100644 index 00000000..50e85494 --- /dev/null +++ b/examples/vhosts.py @@ -0,0 +1,18 @@ +from sanic.response import text +from sanic import Sanic + +# Usage +# curl -H "Host: example.com" localhost:8000 +# curl -H "Host: sub.example.com" localhost:8000 + +app = Sanic() + +@app.route('/', host="example.com") +async def hello(request): + return text("Answer") +@app.route('/', host="sub.example.com") +async def hello(request): + return text("42") + +if __name__ == '__main__': + app.run(host="0.0.0.0", port=8000) diff --git a/sanic/router.py b/sanic/router.py index 081b5da6..a1f47230 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -55,8 +55,9 @@ class Router: self.routes_static = {} self.routes_dynamic = defaultdict(list) self.routes_always_check = [] + self.hosts = None - def add(self, uri, methods, handler): + def add(self, uri, methods, handler, host=None): """ Adds a handler to the route list :param uri: Path to match @@ -66,6 +67,17 @@ class Router: When executed, it should provide a response object. :return: Nothing """ + + if host is not None: + # we want to track if there are any + # vhosts on the Router instance so that we can + # default to the behavior without vhosts + if self.hosts is None: + self.hosts = set(host) + else: + self.hosts.add(host) + uri = host + uri + if uri in self.routes_all: raise RouteExists("Route already registered: {}".format(uri)) @@ -113,7 +125,9 @@ class Router: else: self.routes_static[uri] = route - def remove(self, uri, clean_cache=True): + def remove(self, uri, clean_cache=True, host=None): + if host is not None: + uri = host + uri try: route = self.routes_all.pop(uri) except KeyError: @@ -137,10 +151,14 @@ class Router: :param request: Request object :return: handler, arguments, keyword arguments """ - return self._get(request.url, request.method) + if self.hosts is None: + return self._get(request.url, request.method, '') + else: + return self._get(request.url, request.method, + request.headers.get("Host", '')) @lru_cache(maxsize=Config.ROUTER_CACHE_SIZE) - def _get(self, url, method): + def _get(self, url, method, host=None): """ Gets a request handler based on the URL of the request, or raises an error. Internal method for caching. @@ -148,6 +166,7 @@ class Router: :param method: Request method :return: handler, arguments, keyword arguments """ + url = host + url # Check against known static routes route = self.routes_static.get(url) if route: diff --git a/sanic/sanic.py b/sanic/sanic.py index 3dab3e47..6926050c 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -51,7 +51,7 @@ class Sanic: # -------------------------------------------------------------------- # # Decorator - def route(self, uri, methods=None): + def route(self, uri, methods=None, host=None): """ Decorates a function to be registered as a route :param uri: path of the URL @@ -65,12 +65,13 @@ class Sanic: uri = '/' + uri def response(handler): - self.router.add(uri=uri, methods=methods, handler=handler) + self.router.add(uri=uri, methods=methods, handler=handler, + host=host) return handler return response - def add_route(self, handler, uri, methods=None): + def add_route(self, handler, uri, methods=None, host=None): """ A helper method to register class instance or functions as a handler to the application url @@ -80,11 +81,11 @@ class Sanic: :param methods: list or tuple of methods allowed :return: function or class instance """ - self.route(uri=uri, methods=methods)(handler) + self.route(uri=uri, methods=methods, host=host)(handler) return handler - def remove_route(self, uri, clean_cache=True): - self.router.remove(uri, clean_cache) + def remove_route(self, uri, clean_cache=True, host=None): + self.router.remove(uri, clean_cache, host) # Decorator def exception(self, *exceptions): diff --git a/tests/test_vhosts.py b/tests/test_vhosts.py new file mode 100644 index 00000000..7bbbb813 --- /dev/null +++ b/tests/test_vhosts.py @@ -0,0 +1,23 @@ +from sanic import Sanic +from sanic.response import json, text +from sanic.utils import sanic_endpoint_test + + +def test_vhosts(): + app = Sanic('test_text') + + @app.route('/', host="example.com") + async def handler(request): + return text("You're at example.com!") + + @app.route('/', host="subdomain.example.com") + async def handler(request): + return text("You're at subdomain.example.com!") + + headers = {"Host": "example.com"} + request, response = sanic_endpoint_test(app, headers=headers) + assert response.text == "You're at example.com!" + + headers = {"Host": "subdomain.example.com"} + request, response = sanic_endpoint_test(app, headers=headers) + assert response.text == "You're at subdomain.example.com!" From 055430d4b848a8a7e60947e49a2170e58af158bd Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 10 Jan 2017 16:01:21 -0800 Subject: [PATCH 49/71] remove default from host in _get method --- sanic/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/router.py b/sanic/router.py index a1f47230..9ab01b1c 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -158,7 +158,7 @@ class Router: request.headers.get("Host", '')) @lru_cache(maxsize=Config.ROUTER_CACHE_SIZE) - def _get(self, url, method, host=None): + def _get(self, url, method, host): """ Gets a request handler based on the URL of the request, or raises an error. Internal method for caching. From 62df50e22bcba5cae476b57b4fa175cfea9625f4 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 10 Jan 2017 18:07:58 -0800 Subject: [PATCH 50/71] add vhosts to blueprints --- examples/vhosts.py | 14 ++++++++++ sanic/blueprints.py | 10 ++++--- tests/test_blueprints.py | 58 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/examples/vhosts.py b/examples/vhosts.py index 50e85494..40dc7ba5 100644 --- a/examples/vhosts.py +++ b/examples/vhosts.py @@ -1,11 +1,15 @@ from sanic.response import text from sanic import Sanic +from sanic.blueprints import Blueprint # Usage # curl -H "Host: example.com" localhost:8000 # curl -H "Host: sub.example.com" localhost:8000 +# curl -H "Host: bp.example.com" localhost:8000/question +# curl -H "Host: bp.example.com" localhost:8000/answer app = Sanic() +bp = Blueprint("bp", host="bp.example.com") @app.route('/', host="example.com") async def hello(request): @@ -14,5 +18,15 @@ async def hello(request): async def hello(request): return text("42") +@bp.route("/question") +async def hello(request): + return text("What is the meaning of life?") + +@bp.route("/answer") +async def hello(request): + return text("42") + +app.register_blueprint(bp) + if __name__ == '__main__': app.run(host="0.0.0.0", port=8000) diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 92e376f1..4cf460f3 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -18,14 +18,17 @@ class BlueprintSetup: #: blueprint. self.url_prefix = url_prefix - def add_route(self, handler, uri, methods): + def add_route(self, handler, uri, methods, host=None): """ A helper method to register a handler to the application url routes. """ if self.url_prefix: uri = self.url_prefix + uri - self.app.route(uri=uri, methods=methods)(handler) + if host is None: + host = self.blueprint.host + + self.app.route(uri=uri, methods=methods, host=host)(handler) def add_exception(self, handler, *args, **kwargs): """ @@ -53,7 +56,7 @@ class BlueprintSetup: class Blueprint: - def __init__(self, name, url_prefix=None): + def __init__(self, name, url_prefix=None, host=None): """ Creates a new blueprint :param name: Unique name of the blueprint @@ -63,6 +66,7 @@ class Blueprint: self.url_prefix = url_prefix self.deferred_functions = [] self.listeners = defaultdict(list) + self.host = host def record(self, func): """ diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index f7b9b8ef..04c5c59d 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -59,6 +59,62 @@ def test_several_bp_with_url_prefix(): request, response = sanic_endpoint_test(app, uri='/test2/') assert response.text == 'Hello2' +def test_bp_with_host(): + app = Sanic('test_bp_host') + bp = Blueprint('test_bp_host', url_prefix='/test1', host="example.com") + + @bp.route('/') + def handler(request): + return text('Hello') + + app.blueprint(bp) + headers = {"Host": "example.com"} + request, response = sanic_endpoint_test(app, uri='/test1/', + headers=headers) + + assert response.text == 'Hello' + + +def test_several_bp_with_host(): + app = Sanic('test_text') + bp = Blueprint('test_text', + url_prefix='/test', + host="example.com") + bp2 = Blueprint('test_text2', + url_prefix='/test', + host="sub.example.com") + + @bp.route('/') + def handler(request): + return text('Hello') + + @bp2.route('/') + def handler2(request): + return text('Hello2') + + @bp2.route('/other/') + def handler2(request): + return text('Hello3') + + + app.blueprint(bp) + app.blueprint(bp2) + + assert bp.host == "example.com" + headers = {"Host": "example.com"} + request, response = sanic_endpoint_test(app, uri='/test/', + headers=headers) + assert response.text == 'Hello' + + assert bp2.host == "sub.example.com" + headers = {"Host": "sub.example.com"} + request, response = sanic_endpoint_test(app, uri='/test/', + headers=headers) + + assert response.text == 'Hello2' + request, response = sanic_endpoint_test(app, uri='/test/other/', + headers=headers) + assert response.text == 'Hello3' def test_bp_middleware(): app = Sanic('test_middleware') @@ -162,4 +218,4 @@ def test_bp_static(): request, response = sanic_endpoint_test(app, uri='/testing.file') assert response.status == 200 - assert response.body == current_file_contents \ No newline at end of file + assert response.body == current_file_contents From 15e4ec7ffb25e33a5dafa5950c18a25062e16425 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 10 Jan 2017 22:08:15 -0800 Subject: [PATCH 51/71] add ability to override default host in blueprint --- sanic/blueprints.py | 8 ++++---- tests/test_blueprints.py | 11 ++++++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 4cf460f3..583aa244 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -87,18 +87,18 @@ class Blueprint: for deferred in self.deferred_functions: deferred(state) - def route(self, uri, methods=None): + def route(self, uri, methods=None, host=None): """ """ def decorator(handler): - self.record(lambda s: s.add_route(handler, uri, methods)) + self.record(lambda s: s.add_route(handler, uri, methods, host)) return handler return decorator - def add_route(self, handler, uri, methods=None): + def add_route(self, handler, uri, methods=None, host=None): """ """ - self.record(lambda s: s.add_route(handler, uri, methods)) + self.record(lambda s: s.add_route(handler, uri, methods, host)) return handler def listener(self, event): diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 04c5c59d..75109e2c 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -67,13 +67,22 @@ def test_bp_with_host(): def handler(request): return text('Hello') + @bp.route('/', host="sub.example.com") + def handler(request): + return text('Hello subdomain!') + app.blueprint(bp) headers = {"Host": "example.com"} request, response = sanic_endpoint_test(app, uri='/test1/', headers=headers) - assert response.text == 'Hello' + headers = {"Host": "sub.example.com"} + request, response = sanic_endpoint_test(app, uri='/test1/', + headers=headers) + + assert response.text == 'Hello subdomain!' + def test_several_bp_with_host(): app = Sanic('test_text') From 9dd954bccde50d942173aaf861fdedb51a502e71 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Wed, 11 Jan 2017 16:55:34 -0600 Subject: [PATCH 52/71] Update request.form to work with __getitem__ --- sanic/request.py | 3 +++ tests/{tests_server_events.py => test_server_events.py} | 0 2 files changed, 3 insertions(+) rename tests/{tests_server_events.py => test_server_events.py} (100%) diff --git a/sanic/request.py b/sanic/request.py index 7ca5523c..5c4a7db4 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -25,6 +25,9 @@ class RequestParameters(dict): self.super = super() self.super.__init__(*args, **kwargs) + def __getitem__(self, name): + return self.get(name) + def get(self, name, default=None): values = self.super.get(name) return values[0] if values else default diff --git a/tests/tests_server_events.py b/tests/test_server_events.py similarity index 100% rename from tests/tests_server_events.py rename to tests/test_server_events.py From 8c5e214131b5f1e5b54af439103cff7b8a1e5973 Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Thu, 12 Jan 2017 19:54:34 -0500 Subject: [PATCH 53/71] html and tests pass --- requirements-dev.txt | 1 + sanic/exceptions.py | 124 +++++++++++++++++++++++++++++-- tests/test_exceptions.py | 9 ++- tests/test_exceptions_handler.py | 24 ++++++ 4 files changed, 152 insertions(+), 6 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1c34d695..8e0ffac1 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,3 +12,4 @@ kyoukai falcon tornado aiofiles +beautifulsoup4 diff --git a/sanic/exceptions.py b/sanic/exceptions.py index b9e6bf00..430f1d29 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -1,6 +1,104 @@ -from .response import text +from .response import text, html from .log import log -from traceback import format_exc +from traceback import format_exc, extract_tb +import sys + +TRACEBACK_STYLE = ''' + +''' + +TRACEBACK_WRAPPER_HTML = ''' + + + {style} + + +

{exc_name}

+

{exc_value}

+
+

Traceback (most recent call last):

+ {frame_html} +

+ {exc_name}: {exc_value} + while handling uri {uri} +

+
+ + +''' + +TRACEBACK_LINE_HTML = ''' +
+

+ File {0.filename}, line {0.lineno}, + in {0.name} +

+

{0.line}

+
+''' + +INTERNAL_SERVER_ERROR_HTML = ''' +

Internal Server Error

+

+ The server encountered an internal error and cannot complete + your request. +

+''' class SanicException(Exception): @@ -46,6 +144,21 @@ class Handler: self.handlers = {} self.sanic = sanic + def _render_traceback_html(self, exception, request): + exc_type, exc_value, tb = sys.exc_info() + frames = extract_tb(tb) + + frame_html = [] + for frame in frames: + frame_html.append(TRACEBACK_LINE_HTML.format(frame)) + + return TRACEBACK_WRAPPER_HTML.format( + style=TRACEBACK_STYLE, + exc_name=exc_type.__name__, + exc_value=exc_value, + frame_html=''.join(frame_html), + uri=request.url) + def add(self, exception, handler): self.handlers[exception] = handler @@ -77,11 +190,12 @@ class Handler: 'Error: {}'.format(exception), status=getattr(exception, 'status_code', 500)) elif self.sanic.debug: + html_output = self._render_traceback_html(exception, request) + response_message = ( 'Exception occurred while handling uri: "{}"\n{}'.format( request.url, format_exc())) log.error(response_message) - return text(response_message, status=500) + return html(html_output, status=500) else: - return text( - 'An error occurred while generating the response', status=500) + return html(INTERNAL_SERVER_ERROR_HTML, status=500) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 5cebfb87..a81e0d09 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,4 +1,5 @@ import pytest +from bs4 import BeautifulSoup from sanic import Sanic from sanic.response import text @@ -75,7 +76,13 @@ def test_handled_unhandled_exception(exception_app): 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' + soup = BeautifulSoup(response.body, 'html.parser') + assert soup.h1.text == 'Internal Server Error' + + message = " ".join(soup.p.text.split()) + assert message == ( + "The server encountered an internal error and " + "cannot complete your request.") def test_exception_in_exception_handler(exception_app): diff --git a/tests/test_exceptions_handler.py b/tests/test_exceptions_handler.py index 2e8bc359..c56713b6 100644 --- a/tests/test_exceptions_handler.py +++ b/tests/test_exceptions_handler.py @@ -2,6 +2,7 @@ from sanic import Sanic from sanic.response import text from sanic.exceptions import InvalidUsage, ServerError, NotFound from sanic.utils import sanic_endpoint_test +from bs4 import BeautifulSoup exception_handler_app = Sanic('test_exception_handler') @@ -21,6 +22,12 @@ def handler_3(request): raise NotFound("OK") +@exception_handler_app.route('/4') +def handler_4(request): + foo = bar + return text(foo) + + @exception_handler_app.exception(NotFound, ServerError) def handler_exception(request, exception): return text("OK") @@ -47,3 +54,20 @@ def test_text_exception__handler(): exception_handler_app, uri='/random') assert response.status == 200 assert response.text == 'OK' + + +def test_html_traceback_output_in_debug_mode(): + request, response = sanic_endpoint_test( + exception_handler_app, uri='/4', debug=True) + assert response.status == 500 + soup = BeautifulSoup(response.body, 'html.parser') + html = str(soup) + + assert 'response = handler(request, *args, **kwargs)' in html + assert 'handler_4' in html + assert 'foo = bar' in html + + summary_text = " ".join(soup.select('.summary')[0].text.split()) + assert ( + "NameError: name 'bar' " + "is not defined while handling uri /4") == summary_text From 6e53fa03f5ea4ef74b95758244c98c1f0ec243f0 Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Thu, 12 Jan 2017 20:03:35 -0500 Subject: [PATCH 54/71] add beautifulsoup4 to tox --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index a2f89206..009d971c 100644 --- a/tox.ini +++ b/tox.ini @@ -13,6 +13,7 @@ python = deps = aiohttp pytest + beautifulsoup4 commands = pytest tests {posargs} From 974c857a97af70e47a7c9a55d66b620d9b2aa8d8 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Thu, 12 Jan 2017 19:22:47 -0800 Subject: [PATCH 55/71] add a list of extensions --- README.md | 1 + docs/extensions.md | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 docs/extensions.md diff --git a/README.md b/README.md index 34565545..e73fefd3 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ if __name__ == "__main__": * [Custom Protocol](docs/custom_protocol.md) * [Testing](docs/testing.md) * [Deploying](docs/deploying.md) + * [Extensions](docs/extensions.md) * [Contributing](docs/contributing.md) * [License](LICENSE) diff --git a/docs/extensions.md b/docs/extensions.md new file mode 100644 index 00000000..dbd379b8 --- /dev/null +++ b/docs/extensions.md @@ -0,0 +1,6 @@ +# Sanic Extensions + +A list of Sanic extensions created by the community. + + * [Sessions](https://github.com/subyraman/sanic_session) -- Support for sessions. Allows using redis, memcache or an in memory store. + * [CORS](https://github.com/ashleysommer/sanic-cors) -- A port of flask-cors. From a86cc32dff6850cf3ebec8f3df07a8d9492e65fc Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Thu, 12 Jan 2017 19:30:22 -0800 Subject: [PATCH 56/71] Update extensions.md Since there have been a few extensions discussed in open issues, it makes sense to start keeping track of them. --- docs/extensions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/extensions.md b/docs/extensions.md index dbd379b8..829ccf99 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -2,5 +2,5 @@ A list of Sanic extensions created by the community. - * [Sessions](https://github.com/subyraman/sanic_session) -- Support for sessions. Allows using redis, memcache or an in memory store. - * [CORS](https://github.com/ashleysommer/sanic-cors) -- A port of flask-cors. + * [Sessions](https://github.com/subyraman/sanic_session) — Support for sessions. Allows using redis, memcache or an in memory store. + * [CORS](https://github.com/ashleysommer/sanic-cors) — A port of flask-cors. From 02b9a0a29765f75e92760558975656d0b361f9b7 Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Sat, 14 Jan 2017 00:41:54 -0500 Subject: [PATCH 57/71] add redirect code from @pcdinh --- sanic/response.py | 26 ++++++++++++++++++ tests/test_requests.py | 62 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index f2eb02e5..f6422d5f 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -169,3 +169,29 @@ async def file(location, mime_type=None, headers=None): headers=headers, content_type=mime_type, body_bytes=out_stream) + + +def redirect(to, headers=None, status=302, + content_type=None): + """ + Aborts execution and causes a 302 redirect (by default). + + :param to: path or fully qualified URL to redirect to + :param headers: optional dict of headers to include in the new request + :param status: status code (int) of the new request, defaults to 302 + :param content_type: + the content type (string) of the response, defaults to None + :returns: the redirecting Response + """ + if not content_type: + content_type = "text/html; charset=utf-8" + + headers = headers or {} + + # According to RFC 7231, a relative URI is now permitted. + headers['Location'] = to + + return HTTPResponse( + status=status, + headers=headers, + content_type=content_type) diff --git a/tests/test_requests.py b/tests/test_requests.py index ead76424..2cb41636 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -1,9 +1,10 @@ from json import loads as json_loads, dumps as json_dumps from sanic import Sanic -from sanic.response import json, text +from sanic.response import json, text, redirect from sanic.utils import sanic_endpoint_test from sanic.exceptions import ServerError +import pytest # ------------------------------------------------------------ # # GET @@ -188,3 +189,62 @@ def test_post_form_multipart_form_data(): request, response = sanic_endpoint_test(app, data=payload, headers=headers) assert request.form.get('test') == 'OK' + + +@pytest.fixture +def redirect_app(): + app = Sanic('test_get_then_redirect_01') + + @app.route('/redirect_init') + async def redirect_init(request): + return redirect("/redirect_target") + + @app.route('/redirect_init_with_301') + async def redirect_init_with_301(request): + return redirect("/redirect_target", status=301) + + @app.route('/redirect_target') + async def redirect_target(request): + return text('OK') + + return app + + +def test_redirect_default_302(redirect_app): + """ + We expect a 302 default status code and the headers to be set. + """ + request, response = sanic_endpoint_test( + redirect_app, method="get", + uri="/redirect_init", + allow_redirects=False) + + assert response.status == 302 + assert response.headers["Location"] == "/redirect_target" + assert response.headers["Content-Type"] == 'text/html; charset=utf-8' + + +def test_redirect_with_301(redirect_app): + """ + Test redirection with a different status code. + """ + request, response = sanic_endpoint_test( + redirect_app, method="get", + uri="/redirect_init_with_301", + allow_redirects=False) + + assert response.status == 301 + assert response.headers["Location"] == "/redirect_target" + + +def test_get_then_redirect_follow_redirect(redirect_app): + """ + With `allow_redirects` we expect a 200. + """ + response = sanic_endpoint_test( + redirect_app, method="get", + uri="/redirect_init", gather_request=False, + allow_redirects=True) + + assert response.status == 200 + assert response.text == 'OK' From 7de3f7aa789bfc4fada08acb8b0b5a3bfc203553 Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Sat, 14 Jan 2017 00:43:30 -0500 Subject: [PATCH 58/71] rename test app --- 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 2cb41636..e88b5e28 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -193,7 +193,7 @@ def test_post_form_multipart_form_data(): @pytest.fixture def redirect_app(): - app = Sanic('test_get_then_redirect_01') + app = Sanic('test_redirection') @app.route('/redirect_init') async def redirect_init(request): From 7a1e089725ed103201bce187fba9d435e9c20d2c Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Sat, 14 Jan 2017 00:45:04 -0500 Subject: [PATCH 59/71] add headers none test --- tests/test_requests.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_requests.py b/tests/test_requests.py index e88b5e28..b2ee8e78 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -224,6 +224,17 @@ def test_redirect_default_302(redirect_app): assert response.headers["Content-Type"] == 'text/html; charset=utf-8' +def test_redirect_headers_none(redirect_app): + request, response = sanic_endpoint_test( + redirect_app, method="get", + uri="/redirect_init", + headers=None, + allow_redirects=False) + + assert response.status == 302 + assert response.headers["Location"] == "/redirect_target" + + def test_redirect_with_301(redirect_app): """ Test redirection with a different status code. From b5bbef09c5649e403e045d049c2df7592fca0dba Mon Sep 17 00:00:00 2001 From: Suby Raman Date: Sat, 14 Jan 2017 00:47:28 -0500 Subject: [PATCH 60/71] add redirect method --- sanic/response.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index f6422d5f..9c7bd2b5 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -172,7 +172,7 @@ async def file(location, mime_type=None, headers=None): def redirect(to, headers=None, status=302, - content_type=None): + content_type="text/html; charset=utf-8"): """ Aborts execution and causes a 302 redirect (by default). @@ -180,12 +180,9 @@ def redirect(to, headers=None, status=302, :param headers: optional dict of headers to include in the new request :param status: status code (int) of the new request, defaults to 302 :param content_type: - the content type (string) of the response, defaults to None + the content type (string) of the response :returns: the redirecting Response """ - if not content_type: - content_type = "text/html; charset=utf-8" - headers = headers or {} # According to RFC 7231, a relative URI is now permitted. From e2a16f96a8638fdb50deae6221dba37be6277242 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 14 Jan 2017 11:24:31 -0600 Subject: [PATCH 61/71] Increment version to 0.2.0 --- sanic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/__init__.py b/sanic/__init__.py index 6b9b3a80..6f529eea 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1,6 +1,6 @@ from .sanic import Sanic from .blueprints import Blueprint -__version__ = '0.1.9' +__version__ = '0.2.0' __all__ = ['Sanic', 'Blueprint'] From 2cf4baddfbfa9589ca99c95df68dc24c91fe9541 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 16 Jan 2017 23:27:50 +0000 Subject: [PATCH 62/71] Moved Remote-Addr header to request.ip so it can be pulled on-demand --- docs/request_data.md | 1 + sanic/request.py | 9 +++++++-- sanic/server.py | 7 ++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/request_data.md b/docs/request_data.md index 8891d07f..bcb62ef9 100644 --- a/docs/request_data.md +++ b/docs/request_data.md @@ -9,6 +9,7 @@ The following request variables are accessible as properties: `request.args` (dict) - Query String variables. Use getlist to get multiple of the same name `request.form` (dict) - Posted form variables. Use getlist to get multiple of the same name `request.body` (bytes) - Posted raw body. To get the raw data, regardless of content type +`request.ip` (str) - IP address of the requester See request.py for more information diff --git a/sanic/request.py b/sanic/request.py index 5c4a7db4..52ec469b 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -41,18 +41,19 @@ class Request(dict): Properties of an HTTP request such as URL, headers, etc. """ __slots__ = ( - 'url', 'headers', 'version', 'method', '_cookies', + 'url', 'headers', 'version', 'method', '_cookies', 'transport', 'query_string', 'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', ) - def __init__(self, url_bytes, headers, version, method): + def __init__(self, url_bytes, headers, version, method, transport): # TODO: Content-Encoding detection url_parsed = parse_url(url_bytes) self.url = url_parsed.path.decode('utf-8') self.headers = headers self.version = version self.method = method + self.transport = transport self.query_string = None if url_parsed.query: self.query_string = url_parsed.query.decode('utf-8') @@ -139,6 +140,10 @@ class Request(dict): self._cookies = {} return self._cookies + @property + def ip(self): + return self.transport.get_extra_info('peername') + File = namedtuple('File', ['type', 'body', 'name']) diff --git a/sanic/server.py b/sanic/server.py index ec207d26..ade02564 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -121,15 +121,12 @@ class HttpProtocol(asyncio.Protocol): self.headers.append((name.decode(), value.decode('utf-8'))) def on_headers_complete(self): - 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, headers=CIMultiDict(self.headers), version=self.parser.get_http_version(), - method=self.parser.get_method().decode() + method=self.parser.get_method().decode(), + transport=self.transport ) def on_body(self, body): From 213580ea7808b3d35e12dcaead03cb443b8a5e49 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 16 Jan 2017 15:45:29 -0800 Subject: [PATCH 63/71] cache the remote IP property --- sanic/request.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sanic/request.py b/sanic/request.py index 52ec469b..fbd41696 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -65,6 +65,7 @@ class Request(dict): self.parsed_files = None self.parsed_args = None self._cookies = None + self._ip = None @property def json(self): @@ -142,7 +143,11 @@ class Request(dict): @property def ip(self): - return self.transport.get_extra_info('peername') + if self._ip is None: + self._ip = self.transport.get_extra_info('peername') + return self._ip + else: + return self._ip File = namedtuple('File', ['type', 'body', 'name']) From 5c344f7efa7baaa951f1a4703fa7c739285a3e10 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Mon, 16 Jan 2017 17:51:56 -0600 Subject: [PATCH 64/71] Remove redundant else --- sanic/request.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index fbd41696..7f3b87eb 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -145,9 +145,7 @@ class Request(dict): def ip(self): if self._ip is None: self._ip = self.transport.get_extra_info('peername') - return self._ip - else: - return self._ip + return self._ip File = namedtuple('File', ['type', 'body', 'name']) From 41918eaf0a8d04d566fb0adbbcbc14f5f5bfe176 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Mon, 16 Jan 2017 16:12:42 -0800 Subject: [PATCH 65/71] Trimmed down features of CIMultiDict --- requirements.txt | 1 - sanic/response.py | 4 ++-- sanic/server.py | 24 +++++++++++++++++++++--- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3acfbb1f..cef8660e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,3 @@ httptools ujson uvloop aiofiles -multidict diff --git a/sanic/response.py b/sanic/response.py index 9c7bd2b5..bb3a1f9d 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -106,11 +106,11 @@ class HTTPResponse: for name, value in self.headers.items(): try: headers += ( - b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))) + b'%b: %b\r\n' % (name.title().encode(), value.encode('utf-8'))) except AttributeError: headers += ( b'%b: %b\r\n' % ( - str(name).encode(), str(value).encode('utf-8'))) + str(name).title().encode(), str(value).encode('utf-8'))) # Try to pull from the common codes first # Speeds up response rate 6% over pulling from all diff --git a/sanic/server.py b/sanic/server.py index ec207d26..62a4c1a7 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -1,7 +1,6 @@ import asyncio from functools import partial from inspect import isawaitable -from multidict import CIMultiDict from signal import SIGINT, SIGTERM from time import time from httptools import HttpRequestParser @@ -18,11 +17,30 @@ from .request import Request from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage +current_time = None + + class Signal: stopped = False -current_time = None +class CIDict(dict): + """ + Case Insensitive dict where all keys are converted to lowercase + This does not maintain the inputted case when calling items() or keys() + in favor of speed, since headers are case insensitive + """ + def get(self, key, default=None): + return super().get(key.casefold(), default) + + def __getitem__(self, key): + return super().__getitem__(key.casefold()) + + def __setitem__(self, key, value): + return super().__setitem__(key.casefold(), value) + + def __contains__(self, key): + return super().__contains__(key.casefold()) class HttpProtocol(asyncio.Protocol): @@ -127,7 +145,7 @@ class HttpProtocol(asyncio.Protocol): self.request = Request( url_bytes=self.url, - headers=CIMultiDict(self.headers), + headers=CIDict(self.headers), version=self.parser.get_http_version(), method=self.parser.get_method().decode() ) From ccbbce003648a8f76e83b84f05e4621070651d44 Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Mon, 16 Jan 2017 16:55:55 -0800 Subject: [PATCH 66/71] Fix header capitalization on input also removed redundant utf-8 in encodes/decodes --- sanic/response.py | 8 ++++---- sanic/server.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index bb3a1f9d..9ec2d91a 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -83,10 +83,10 @@ class HTTPResponse: if body is not None: try: # Try to encode it regularly - self.body = body.encode('utf-8') + self.body = body.encode() except AttributeError: # Convert it to a str if you can't - self.body = str(body).encode('utf-8') + self.body = str(body).encode() else: self.body = body_bytes @@ -106,11 +106,11 @@ class HTTPResponse: for name, value in self.headers.items(): try: headers += ( - b'%b: %b\r\n' % (name.title().encode(), value.encode('utf-8'))) + b'%b: %b\r\n' % (name.encode(), value.encode())) except AttributeError: headers += ( b'%b: %b\r\n' % ( - str(name).title().encode(), str(value).encode('utf-8'))) + str(name).encode(), str(value).encode())) # Try to pull from the common codes first # Speeds up response rate 6% over pulling from all diff --git a/sanic/server.py b/sanic/server.py index 62a4c1a7..6d924374 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -136,7 +136,7 @@ class HttpProtocol(asyncio.Protocol): exception = PayloadTooLarge('Payload Too Large') self.write_error(exception) - self.headers.append((name.decode(), value.decode('utf-8'))) + self.headers.append((name.decode().casefold(), value.decode())) def on_headers_complete(self): remote_addr = self.transport.get_extra_info('peername') From 638fbcb619d8a316c4d4c9440999c886d7e5b9ab Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Mon, 16 Jan 2017 17:03:55 -0800 Subject: [PATCH 67/71] Encoding needs a default --- sanic/response.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index 9ec2d91a..ba10b8c4 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -106,11 +106,11 @@ class HTTPResponse: for name, value in self.headers.items(): try: headers += ( - b'%b: %b\r\n' % (name.encode(), value.encode())) + 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())) + 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 9bc69f7de96f030c95bc4d1861bf25d2ec96e294 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 16 Jan 2017 17:21:57 -0800 Subject: [PATCH 68/71] use hasattr --- sanic/request.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 7f3b87eb..26176687 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -65,7 +65,6 @@ class Request(dict): self.parsed_files = None self.parsed_args = None self._cookies = None - self._ip = None @property def json(self): @@ -143,7 +142,7 @@ class Request(dict): @property def ip(self): - if self._ip is None: + if not hasattr(self, '_ip'): self._ip = self.transport.get_extra_info('peername') return self._ip From 5903dd293909c56c697fa7972303028afb7e3884 Mon Sep 17 00:00:00 2001 From: Guilherme Polo Date: Tue, 17 Jan 2017 02:58:45 -0200 Subject: [PATCH 69/71] cannot use the new .ip without updating __slots__ --- sanic/request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sanic/request.py b/sanic/request.py index 26176687..0cd7c738 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -44,6 +44,7 @@ class Request(dict): 'url', 'headers', 'version', 'method', '_cookies', 'transport', 'query_string', 'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', + '_ip', ) def __init__(self, url_bytes, headers, version, method, transport): From ba1e00658578b27316d676dcaeedd1acb9bd23b6 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 17 Jan 2017 15:38:20 -0800 Subject: [PATCH 70/71] update logging placement --- examples/override_logging.py | 2 +- sanic/sanic.py | 22 ++++++++++++++-------- tests/test_logging.py | 2 +- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/examples/override_logging.py b/examples/override_logging.py index 25fd78de..117d63bf 100644 --- a/examples/override_logging.py +++ b/examples/override_logging.py @@ -14,7 +14,7 @@ logging.basicConfig( log = logging.getLogger() # Set logger to override default basicConfig -sanic = Sanic(logger=True) +sanic = Sanic() @sanic.route("/") def test(request): log.info("received request; responding with 'hey'") diff --git a/sanic/sanic.py b/sanic/sanic.py index 6926050c..5f8aeeb2 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -21,12 +21,7 @@ from os import set_inheritable class Sanic: 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" - ) + error_handler=None): if name is None: frame_records = stack()[1] name = getmodulename(frame_records[1]) @@ -154,7 +149,8 @@ class Sanic: def register_blueprint(self, *args, **kwargs): # TODO: deprecate 1.0 log.warning("Use of register_blueprint will be deprecated in " - "version 1.0. Please use the blueprint method instead") + "version 1.0. Please use the blueprint method instead", + DeprecationWarning) return self.blueprint(*args, **kwargs) # -------------------------------------------------------------------- # @@ -247,7 +243,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, backlog=100, - stop_event=None): + stop_event=None, logger=None): """ Runs the HTTP Server and listens until keyboard interrupt or term signal. On termination, drains connections before closing. @@ -269,6 +265,10 @@ class Sanic: :param protocol: Subclass of asyncio protocol class :return: Nothing """ + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s: %(levelname)s: %(message)s" + ) self.error_handler.debug = True self.debug = debug self.loop = loop @@ -350,6 +350,12 @@ class Sanic: :param stop_event: if provided, is used as a stop signal :return: """ + # In case this is called directly, we configure logging here too. + # This won't interfere with the same call from run() + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s: %(levelname)s: %(message)s" + ) server_settings['reuse_port'] = True # Create a stop event to be triggered by a signal diff --git a/tests/test_logging.py b/tests/test_logging.py index 65de28c2..b3e3c1fc 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -19,7 +19,7 @@ def test_log(): stream=log_stream ) log = logging.getLogger() - app = Sanic('test_logging', logger=True) + app = Sanic('test_logging') @app.route('/') def handler(request): log.info('hello world') From 573d1da0efa1812b78a137a78e28b5db8cbb8559 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Tue, 17 Jan 2017 18:28:22 -0600 Subject: [PATCH 71/71] Fixes write_error loop from bail_out function Fixes stack-overflow found in #307 --- sanic/server.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 711117dc..a36c47aa 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -1,4 +1,5 @@ import asyncio +import traceback from functools import partial from inspect import isawaitable from signal import SIGINT, SIGTERM @@ -189,9 +190,15 @@ class HttpProtocol(asyncio.Protocol): "Writing error failed, connection closed {}".format(e)) def bail_out(self, message): - exception = ServerError(message) - self.write_error(exception) - log.error(message) + if self.transport.is_closing(): + log.error( + "Connection closed before error was sent to user @ {}".format( + self.transport.get_extra_info('peername'))) + log.debug('Error experienced:\n{}'.format(traceback.format_exc())) + else: + exception = ServerError(message) + self.write_error(exception) + log.error(message) def cleanup(self): self.parser = None