From 391639210e046a38365e26667cfe654e47767c21 Mon Sep 17 00:00:00 2001 From: Denis Makogon Date: Tue, 15 Jan 2019 13:38:47 +0200 Subject: [PATCH 01/10] make Sanic.create_server return an asyncio.Server - adding 2 new parameters to Sanic.create_server: * return_asyncio_server=False - defines whether there's a need to return an asyncio.Server or run it right away * asyncio_server_kwargs=None - for python 3.7 uvloop doesn't support all necessary features like "start_serving", so, in order to make sanic work well with asyncio from 3.7 there's a need to introduce generic way for passing kwargs for "loop.create_server" Closes: #1469 --- sanic/app.py | 16 ++++++++++++++-- sanic/server.py | 8 +++++++- tests/test_app.py | 5 +++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index c1a39413..bb2561e6 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1121,6 +1121,8 @@ class Sanic: backlog: int = 100, stop_event: Any = None, access_log: Optional[bool] = None, + return_asyncio_server=False, + asyncio_server_kwargs=None, ) -> None: """ Asynchronous version of :func:`run`. @@ -1154,6 +1156,13 @@ class Sanic: :type stop_event: None :param access_log: Enables writing access logs (slows server) :type access_log: bool + :param return_asyncio_server: flag that defines whether there's a need + to return asyncio.Server or + start it serving right away + :type return_asyncio_server: bool + :param asyncio_server_kwargs: key-value arguments for + asyncio/uvloop create_server method + :type asyncio_server_kwargs: dict :return: Nothing """ @@ -1184,7 +1193,7 @@ class Sanic: loop=get_event_loop(), protocol=protocol, backlog=backlog, - run_async=True, + run_async=return_asyncio_server, ) # Trigger before_start events @@ -1193,7 +1202,10 @@ class Sanic: server_settings.get("loop"), ) - return await serve(**server_settings) + return await serve( + asyncio_server_kwargs=asyncio_server_kwargs, + **server_settings + ) async def trigger_events(self, events, loop): """Trigger events (functions or async) diff --git a/sanic/server.py b/sanic/server.py index e3879a80..605e35e3 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -656,6 +656,7 @@ def serve( websocket_write_limit=2 ** 16, state=None, graceful_shutdown_timeout=15.0, + asyncio_server_kwargs=None, ): """Start asynchronous HTTP Server on an individual process. @@ -700,6 +701,8 @@ def serve( :param router: Router object :param graceful_shutdown_timeout: How long take to Force close non-idle connection + :param asyncio_server_kwargs: key-value args for asyncio/uvloop + create_server method :return: Nothing """ if not run_async: @@ -734,7 +737,9 @@ def serve( state=state, debug=debug, ) - + asyncio_server_kwargs = ( + asyncio_server_kwargs if asyncio_server_kwargs else {} + ) server_coroutine = loop.create_server( server, host, @@ -743,6 +748,7 @@ def serve( reuse_port=reuse_port, sock=sock, backlog=backlog, + **asyncio_server_kwargs ) # Instead of pulling time at the end of every request, diff --git a/tests/test_app.py b/tests/test_app.py index be2a45da..e27a4373 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -17,6 +17,11 @@ def test_app_loop_running(app): assert response.text == "pass" +def test_create_asyncio_server(app): + asyncio_srv = app.create_server(return_asyncio_server=True) + assert isinstance(asyncio_srv, asyncio.AbstractServer) + + def test_app_loop_not_running(app): with pytest.raises(SanicException) as excinfo: app.loop From 2cb05ab865237fe5719aad2cae54c50573e7738b Mon Sep 17 00:00:00 2001 From: Denis Makogon Date: Tue, 15 Jan 2019 14:52:53 +0200 Subject: [PATCH 02/10] More tests, attempting to fix CI --- tests/test_app.py | 23 +++++++++++++++++++++-- tests/test_keep_alive_timeout.py | 4 +++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index e27a4373..35967428 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,6 +1,7 @@ import asyncio import logging +from inspect import isawaitable import pytest from sanic.exceptions import SanicException @@ -18,8 +19,26 @@ def test_app_loop_running(app): def test_create_asyncio_server(app): - asyncio_srv = app.create_server(return_asyncio_server=True) - assert isinstance(asyncio_srv, asyncio.AbstractServer) + loop = asyncio.get_event_loop() + asyncio_srv_coro = app.create_server(return_asyncio_server=True) + assert isawaitable(asyncio_srv_coro) + srv = loop.run_until_complete(asyncio_srv_coro) + assert isinstance(srv, asyncio.AbstractServer) + + +def test_asyncio_server_start_serving(app): + try: + import uvloop + except ImportError: + # this test is valid only for sanic + asyncio setup + loop = asyncio.get_event_loop() + asyncio_srv_coro = app.create_server( + return_asyncio_server=True, + asyncio_server_kwargs=dict( + start_serving=False + )) + srv = loop.run_until_complete(asyncio_srv_coro) + assert srv.is_serving() is False def test_app_loop_not_running(app): diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py index 6893f652..af231718 100644 --- a/tests/test_keep_alive_timeout.py +++ b/tests/test_keep_alive_timeout.py @@ -51,7 +51,9 @@ class ReuseableSanicTestClient(SanicTestClient): uri="/", gather_request=True, debug=False, - server_kwargs={}, + server_kwargs={ + "return_asyncio_server": True, + }, *request_args, **request_kwargs ): From b89c5338182ad76a6aa44bfecaafe993081ed1a5 Mon Sep 17 00:00:00 2001 From: Denis Makogon Date: Tue, 15 Jan 2019 15:04:30 +0200 Subject: [PATCH 03/10] Adding doc --- docs/index.rst | 1 + docs/sanic/asyncio_python37.rst | 58 +++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 docs/sanic/asyncio_python37.rst diff --git a/docs/index.rst b/docs/index.rst index 4123d32c..180ef1ae 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -33,6 +33,7 @@ Guides sanic/changelog sanic/contributing sanic/api_reference + sanic/asyncio_python37 Module Documentation diff --git a/docs/sanic/asyncio_python37.rst b/docs/sanic/asyncio_python37.rst new file mode 100644 index 00000000..1b09c1b3 --- /dev/null +++ b/docs/sanic/asyncio_python37.rst @@ -0,0 +1,58 @@ +Python 3.7 AsyncIO examples +########################### + +With Python 3.7 AsyncIO got major update for the following types: + + - asyncio.AbstractEventLoop + - asyncio.AnstractServer + + +This example shows how to use sanic with Python 3.7, to be precise: how to retrieve an asyncio server instance: + +.. code:: python + + import asyncio + import socket + import os + + from sanic import Sanic + from sanic.response import json + + app = Sanic(__name__) + + + @app.route("/") + async def test(request): + return json({"hello": "world"}) + + + server_socket = '/tmp/sanic.sock' + + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + + try: + os.remote(server_socket) + finally: + sock.bind(server_socket) + + if __name__ == "__main__": + loop = asyncio.get_event_loop() + srv_coro = app.create_server( + sock=sock, + return_asyncio_server=True, + asyncio_server_args=dict( + start_serving=False + ) + ) + srv = loop.run_until_complete(srv_coro) + try: + assert srv.is_serving() is False + loop.run_until_complete(srv.start_serving()) + assert srv.is_serving() is True + loop.run_until_complete(srv.serve_forever()) + except KeyboardInterrupt: + srv.close() + loop.close() + + +Please note that uvloop does not support these features yet. From 0242bc999f0cd4448b9175a45b117a740294ff45 Mon Sep 17 00:00:00 2001 From: Denis Makogon Date: Tue, 15 Jan 2019 15:11:38 +0200 Subject: [PATCH 04/10] Fix type asserting --- tests/test_app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_app.py b/tests/test_app.py index 35967428..91cb9ca5 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,6 +1,8 @@ import asyncio import logging +from asyncio import base_events as asyncio_base_events + from inspect import isawaitable import pytest @@ -23,7 +25,7 @@ def test_create_asyncio_server(app): asyncio_srv_coro = app.create_server(return_asyncio_server=True) assert isawaitable(asyncio_srv_coro) srv = loop.run_until_complete(asyncio_srv_coro) - assert isinstance(srv, asyncio.AbstractServer) + assert isinstance(srv, asyncio_base_events.Server) def test_asyncio_server_start_serving(app): From eed22a7a24289d14bb64bd5f360c6fa3fc66132c Mon Sep 17 00:00:00 2001 From: Denis Makogon Date: Tue, 15 Jan 2019 15:17:57 +0200 Subject: [PATCH 05/10] Fix app.create_server calls --- tests/test_app.py | 2 +- tests/test_config.py | 6 ++++-- tests/test_logo.py | 12 ++++++++---- tests/test_server_events.py | 3 ++- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 91cb9ca5..715b7a6f 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -25,7 +25,7 @@ def test_create_asyncio_server(app): asyncio_srv_coro = app.create_server(return_asyncio_server=True) assert isawaitable(asyncio_srv_coro) srv = loop.run_until_complete(asyncio_srv_coro) - assert isinstance(srv, asyncio_base_events.Server) + assert srv.is_serving() is True def test_asyncio_server_start_serving(app): diff --git a/tests/test_config.py b/tests/test_config.py index fe4b97b9..241a2566 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -231,10 +231,12 @@ async def test_config_access_log_passing_in_create_server(app): async def _request(sanic, loop): app.stop() - await app.create_server(port=1341, access_log=False) + await app.create_server(port=1341, access_log=False, + return_asyncio_server=True) assert app.config.ACCESS_LOG == False - await app.create_server(port=1342, access_log=True) + await app.create_server(port=1342, access_log=True, + return_asyncio_server=True) assert app.config.ACCESS_LOG == True diff --git a/tests/test_logo.py b/tests/test_logo.py index 1deb46f6..901d7b8d 100644 --- a/tests/test_logo.py +++ b/tests/test_logo.py @@ -12,7 +12,8 @@ except BaseException: def test_logo_base(app, caplog): - server = app.create_server(debug=True) + server = app.create_server( + debug=True, return_asyncio_server=True) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop._stopping = False @@ -31,7 +32,8 @@ def test_logo_base(app, caplog): def test_logo_false(app, caplog): app.config.LOGO = False - server = app.create_server(debug=True) + server = app.create_server( + debug=True, return_asyncio_server=True) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop._stopping = False @@ -50,7 +52,8 @@ def test_logo_false(app, caplog): def test_logo_true(app, caplog): app.config.LOGO = True - server = app.create_server(debug=True) + server = app.create_server( + debug=True, return_asyncio_server=True) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop._stopping = False @@ -69,7 +72,8 @@ def test_logo_true(app, caplog): def test_logo_custom(app, caplog): app.config.LOGO = "My Custom Logo" - server = app.create_server(debug=True) + server = app.create_server( + debug=True, return_asyncio_server=True) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop._stopping = False diff --git a/tests/test_server_events.py b/tests/test_server_events.py index 0dc344df..2e2cf513 100644 --- a/tests/test_server_events.py +++ b/tests/test_server_events.py @@ -83,7 +83,8 @@ async def test_trigger_before_events_create_server(app): async def init_db(app, loop): app.db = MySanicDb() - await app.create_server() + await app.create_server( + debug=True, return_asyncio_server=True) assert hasattr(app, "db") assert isinstance(app.db, MySanicDb) From 757974714e59c49aca6a6910d259d7e5a7eb4f6a Mon Sep 17 00:00:00 2001 From: Denis Makogon Date: Tue, 15 Jan 2019 17:27:41 +0200 Subject: [PATCH 06/10] skip tests if python version is not 3.7 at least --- tests/test_app.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 715b7a6f..7274d3d9 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,7 +1,6 @@ import asyncio import logging - -from asyncio import base_events as asyncio_base_events +import sys from inspect import isawaitable import pytest @@ -20,14 +19,19 @@ def test_app_loop_running(app): assert response.text == "pass" +@pytest.mark.skipif(sys.version_info < (3, 7), + reason="requires python3.7 or higher") def test_create_asyncio_server(app): loop = asyncio.get_event_loop() - asyncio_srv_coro = app.create_server(return_asyncio_server=True) + asyncio_srv_coro = app.create_server( + return_asyncio_server=True) assert isawaitable(asyncio_srv_coro) srv = loop.run_until_complete(asyncio_srv_coro) assert srv.is_serving() is True +@pytest.mark.skipif(sys.version_info < (3, 7), + reason="requires python3.7 or higher") def test_asyncio_server_start_serving(app): try: import uvloop From 1af16836d4a7acca614bf6f4d67f4bf1c3debdb9 Mon Sep 17 00:00:00 2001 From: Denis Makogon Date: Tue, 15 Jan 2019 17:30:32 +0200 Subject: [PATCH 07/10] make tests dependent on uvloop --- tests/test_app.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 7274d3d9..68a98b2c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -26,8 +26,13 @@ def test_create_asyncio_server(app): asyncio_srv_coro = app.create_server( return_asyncio_server=True) assert isawaitable(asyncio_srv_coro) - srv = loop.run_until_complete(asyncio_srv_coro) - assert srv.is_serving() is True + + try: + import uvloop + except ImportError: + # this test is valid only for sanic + asyncio setup + srv = loop.run_until_complete(asyncio_srv_coro) + assert srv.is_serving() is True @pytest.mark.skipif(sys.version_info < (3, 7), From f8f0241c271277cdf4a45b38fb6686217285a30a Mon Sep 17 00:00:00 2001 From: Denis Makogon Date: Tue, 15 Jan 2019 17:32:40 +0200 Subject: [PATCH 08/10] refactor uvloop detection in its own method --- tests/test_app.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 68a98b2c..848e3f88 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -9,6 +9,14 @@ from sanic.exceptions import SanicException from sanic.response import text +def uvloop_installed(): + try: + import uvloop + return True + except ImportError: + return False + + def test_app_loop_running(app): @app.get("/test") async def handler(request): @@ -19,37 +27,28 @@ def test_app_loop_running(app): assert response.text == "pass" -@pytest.mark.skipif(sys.version_info < (3, 7), +@pytest.mark.skipif(sys.version_info < (3, 7) and uvloop_installed(), reason="requires python3.7 or higher") def test_create_asyncio_server(app): loop = asyncio.get_event_loop() asyncio_srv_coro = app.create_server( return_asyncio_server=True) assert isawaitable(asyncio_srv_coro) - - try: - import uvloop - except ImportError: - # this test is valid only for sanic + asyncio setup - srv = loop.run_until_complete(asyncio_srv_coro) - assert srv.is_serving() is True + srv = loop.run_until_complete(asyncio_srv_coro) + assert srv.is_serving() is True -@pytest.mark.skipif(sys.version_info < (3, 7), +@pytest.mark.skipif(sys.version_info < (3, 7) and uvloop_installed(), reason="requires python3.7 or higher") def test_asyncio_server_start_serving(app): - try: - import uvloop - except ImportError: - # this test is valid only for sanic + asyncio setup - loop = asyncio.get_event_loop() - asyncio_srv_coro = app.create_server( - return_asyncio_server=True, - asyncio_server_kwargs=dict( - start_serving=False - )) - srv = loop.run_until_complete(asyncio_srv_coro) - assert srv.is_serving() is False + loop = asyncio.get_event_loop() + asyncio_srv_coro = app.create_server( + return_asyncio_server=True, + asyncio_server_kwargs=dict( + start_serving=False + )) + srv = loop.run_until_complete(asyncio_srv_coro) + assert srv.is_serving() is False def test_app_loop_not_running(app): From b36bd218133c1008a33f8668adc9b11046c2cb5e Mon Sep 17 00:00:00 2001 From: Denis Makogon Date: Tue, 15 Jan 2019 17:45:47 +0200 Subject: [PATCH 09/10] fix uvloop check --- tests/test_app.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 848e3f88..ba21238c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -27,28 +27,30 @@ def test_app_loop_running(app): assert response.text == "pass" -@pytest.mark.skipif(sys.version_info < (3, 7) and uvloop_installed(), +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") def test_create_asyncio_server(app): - loop = asyncio.get_event_loop() - asyncio_srv_coro = app.create_server( - return_asyncio_server=True) - assert isawaitable(asyncio_srv_coro) - srv = loop.run_until_complete(asyncio_srv_coro) - assert srv.is_serving() is True + if not uvloop_installed(): + loop = asyncio.get_event_loop() + asyncio_srv_coro = app.create_server( + return_asyncio_server=True) + assert isawaitable(asyncio_srv_coro) + srv = loop.run_until_complete(asyncio_srv_coro) + assert srv.is_serving() is True -@pytest.mark.skipif(sys.version_info < (3, 7) and uvloop_installed(), +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") def test_asyncio_server_start_serving(app): - loop = asyncio.get_event_loop() - asyncio_srv_coro = app.create_server( - return_asyncio_server=True, - asyncio_server_kwargs=dict( - start_serving=False - )) - srv = loop.run_until_complete(asyncio_srv_coro) - assert srv.is_serving() is False + if not uvloop_installed(): + loop = asyncio.get_event_loop() + asyncio_srv_coro = app.create_server( + return_asyncio_server=True, + asyncio_server_kwargs=dict( + start_serving=False + )) + srv = loop.run_until_complete(asyncio_srv_coro) + assert srv.is_serving() is False def test_app_loop_not_running(app): From 1473753d43b4a5b4548ae827bcaeeaf2e6ca4208 Mon Sep 17 00:00:00 2001 From: Denis Makogon Date: Tue, 15 Jan 2019 17:48:26 +0200 Subject: [PATCH 10/10] linter fix --- sanic/app.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index bb2561e6..e7ef89e0 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1203,8 +1203,7 @@ class Sanic: ) return await serve( - asyncio_server_kwargs=asyncio_server_kwargs, - **server_settings + asyncio_server_kwargs=asyncio_server_kwargs, **server_settings ) async def trigger_events(self, events, loop):