From c657c531b482039c18bcf926052fcc1f6c8377d0 Mon Sep 17 00:00:00 2001 From: 38elements Date: Fri, 23 Dec 2016 00:13:38 +0900 Subject: [PATCH 1/8] 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 39211f8fbddb997f001f8ffc2059a0a0ad729125 Mon Sep 17 00:00:00 2001 From: 38elements Date: Sat, 24 Dec 2016 11:40:07 +0900 Subject: [PATCH 2/8] 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 3/8] 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 ac44900fc40f3296ba858eaf371a1213339f2db2 Mon Sep 17 00:00:00 2001 From: 38elements Date: Mon, 26 Dec 2016 23:41:10 +0900 Subject: [PATCH 4/8] 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 5/8] 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 83e9d0885373e6d5f43a328e861d2321c769e62b Mon Sep 17 00:00:00 2001 From: 38elements Date: Thu, 29 Dec 2016 13:11:27 +0900 Subject: [PATCH 6/8] 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 7/8] 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 8/8] 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