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. diff --git a/sanic/app.py b/sanic/app.py index c1a39413..e7ef89e0 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,9 @@ 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..ba21238c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,12 +1,22 @@ import asyncio import logging +import sys +from inspect import isawaitable import pytest 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): @@ -17,6 +27,32 @@ 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): + 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), + reason="requires python3.7 or higher") +def test_asyncio_server_start_serving(app): + 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): with pytest.raises(SanicException) as excinfo: app.loop 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_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 ): 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)