From 283911d0b21a9d29514ca7ec67d6b8fe1714edeb Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sun, 3 Oct 2021 20:42:04 +0200 Subject: [PATCH 01/56] Make use of uvloop optional --- sanic/app.py | 5 ++++- sanic/config.py | 2 ++ sanic/server/__init__.py | 17 +++++++++++------ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 0686f7ed7c..1704aa6730 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -73,7 +73,7 @@ from sanic.router import Router from sanic.server import AsyncioServer, HttpProtocol from sanic.server import Signal as ServerSignal -from sanic.server import serve, serve_multiple, serve_single +from sanic.server import serve, serve_multiple, serve_single, use_uvloop from sanic.server.protocols.websocket_protocol import WebSocketProtocol from sanic.server.websockets.impl import ConnectionClosed from sanic.signals import Signal, SignalRouter @@ -206,6 +206,9 @@ def __init__( if self.config.REGISTER: self.__class__.register_app(self) + if self.config.USE_UVLOOP: + use_uvloop() + self.router.ctx.app = self self.signal_router.ctx.app = self diff --git a/sanic/config.py b/sanic/config.py index 649d9414bc..3a7c280ab0 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -36,6 +36,7 @@ "REQUEST_MAX_SIZE": 100000000, # 100 megabytes "REQUEST_TIMEOUT": 60, # 60 seconds "RESPONSE_TIMEOUT": 60, # 60 seconds + "USE_UVLOOP": True, "WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte "WEBSOCKET_PING_INTERVAL": 20, "WEBSOCKET_PING_TIMEOUT": 20, @@ -61,6 +62,7 @@ class Config(dict): REQUEST_TIMEOUT: int RESPONSE_TIMEOUT: int SERVER_NAME: str + USE_UVLOOP: bool WEBSOCKET_MAX_SIZE: int WEBSOCKET_PING_INTERVAL: int WEBSOCKET_PING_TIMEOUT: int diff --git a/sanic/server/__init__.py b/sanic/server/__init__.py index 8e26dcd021..4c63d17e45 100644 --- a/sanic/server/__init__.py +++ b/sanic/server/__init__.py @@ -6,13 +6,18 @@ from sanic.server.runners import serve, serve_multiple, serve_single -try: - import uvloop # type: ignore +def use_uvloop(): + """ + Use uvloop (if available) instead of the default + asyncio loop. + """ + try: + import uvloop # type: ignore - if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy): - asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -except ImportError: - pass + if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy): + asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + except ImportError: + pass __all__ = ( From 1a034fc9c4bd027e42b33a7ebeac5eca1fca33ef Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sun, 3 Oct 2021 20:51:32 +0200 Subject: [PATCH 02/56] Replace `USE_UVLOOP` with `NO_UVLOOP` for consistency --- sanic/app.py | 2 +- sanic/config.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 1704aa6730..5d1e5547f3 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -206,7 +206,7 @@ def __init__( if self.config.REGISTER: self.__class__.register_app(self) - if self.config.USE_UVLOOP: + if not self.config.NO_UVLOOP: use_uvloop() self.router.ctx.app = self diff --git a/sanic/config.py b/sanic/config.py index 3a7c280ab0..310a289b66 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -27,6 +27,7 @@ "GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec "KEEP_ALIVE_TIMEOUT": 5, # 5 seconds "KEEP_ALIVE": True, + "NO_UVLOOP": False, "PROXIES_COUNT": None, "REAL_IP_HEADER": None, "REGISTER": True, @@ -36,7 +37,6 @@ "REQUEST_MAX_SIZE": 100000000, # 100 megabytes "REQUEST_TIMEOUT": 60, # 60 seconds "RESPONSE_TIMEOUT": 60, # 60 seconds - "USE_UVLOOP": True, "WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte "WEBSOCKET_PING_INTERVAL": 20, "WEBSOCKET_PING_TIMEOUT": 20, @@ -52,6 +52,7 @@ class Config(dict): GRACEFUL_SHUTDOWN_TIMEOUT: float KEEP_ALIVE_TIMEOUT: int KEEP_ALIVE: bool + NO_UVLOOP: bool PROXIES_COUNT: Optional[int] REAL_IP_HEADER: Optional[str] REGISTER: bool @@ -62,7 +63,6 @@ class Config(dict): REQUEST_TIMEOUT: int RESPONSE_TIMEOUT: int SERVER_NAME: str - USE_UVLOOP: bool WEBSOCKET_MAX_SIZE: int WEBSOCKET_PING_INTERVAL: int WEBSOCKET_PING_TIMEOUT: int From 6f776c1a2e76d56ec1036451ef7fb7ab10928c00 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sun, 3 Oct 2021 20:56:14 +0200 Subject: [PATCH 03/56] Fix linting --- sanic/server/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sanic/server/__init__.py b/sanic/server/__init__.py index 4c63d17e45..621ea957a1 100644 --- a/sanic/server/__init__.py +++ b/sanic/server/__init__.py @@ -14,7 +14,9 @@ def use_uvloop(): try: import uvloop # type: ignore - if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy): + if not isinstance( + asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy + ): asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) except ImportError: pass From e6e0cd6528291fa879d8a69b50423182e50f6429 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Mon, 4 Oct 2021 20:20:57 +0200 Subject: [PATCH 04/56] Rename `NO_UVLOOP` to `USE_UVLOOP` in config --- sanic/app.py | 2 +- sanic/config.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 5d1e5547f3..1704aa6730 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -206,7 +206,7 @@ def __init__( if self.config.REGISTER: self.__class__.register_app(self) - if not self.config.NO_UVLOOP: + if self.config.USE_UVLOOP: use_uvloop() self.router.ctx.app = self diff --git a/sanic/config.py b/sanic/config.py index 310a289b66..847815abce 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -27,7 +27,6 @@ "GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec "KEEP_ALIVE_TIMEOUT": 5, # 5 seconds "KEEP_ALIVE": True, - "NO_UVLOOP": False, "PROXIES_COUNT": None, "REAL_IP_HEADER": None, "REGISTER": True, @@ -37,6 +36,7 @@ "REQUEST_MAX_SIZE": 100000000, # 100 megabytes "REQUEST_TIMEOUT": 60, # 60 seconds "RESPONSE_TIMEOUT": 60, # 60 seconds + "USE_UVLOOP": False, "WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte "WEBSOCKET_PING_INTERVAL": 20, "WEBSOCKET_PING_TIMEOUT": 20, @@ -52,7 +52,6 @@ class Config(dict): GRACEFUL_SHUTDOWN_TIMEOUT: float KEEP_ALIVE_TIMEOUT: int KEEP_ALIVE: bool - NO_UVLOOP: bool PROXIES_COUNT: Optional[int] REAL_IP_HEADER: Optional[str] REGISTER: bool @@ -63,6 +62,7 @@ class Config(dict): REQUEST_TIMEOUT: int RESPONSE_TIMEOUT: int SERVER_NAME: str + USE_UVLOOP: bool WEBSOCKET_MAX_SIZE: int WEBSOCKET_PING_INTERVAL: int WEBSOCKET_PING_TIMEOUT: int From b3ff3926dbf4bb3db6b172ff0ce0e75aedac1168 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sat, 9 Oct 2021 19:41:50 +0200 Subject: [PATCH 05/56] Added warning messages --- sanic/app.py | 26 ++++++++++++++++++++++++-- sanic/server/__init__.py | 5 +++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 1704aa6730..26379f6a78 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -15,6 +15,7 @@ ) from asyncio.futures import Future from collections import defaultdict, deque +from distutils.util import strtobool from functools import partial from inspect import isawaitable from pathlib import Path @@ -49,6 +50,7 @@ from sanic.base import BaseSanic from sanic.blueprint_group import BlueprintGroup from sanic.blueprints import Blueprint +from sanic.compat import OS_IS_WINDOWS from sanic.config import BASE_LOGO, SANIC_PREFIX, Config from sanic.exceptions import ( InvalidUsage, @@ -206,8 +208,28 @@ def __init__( if self.config.REGISTER: self.__class__.register_app(self) - if self.config.USE_UVLOOP: - use_uvloop() + if self.config.USE_UVLOOP and not OS_IS_WINDOWS: + uvloop_success = use_uvloop() + + # uvloop requested, but not installed + if not uvloop_success: + error_logger.warning( + "You are trying to use uvloop, but uvloop is not " + "installed in your system. In order to use uvloop " + "you must first install it. Otherwise, you can disable " + "uvloop completely by setting the 'USE_UVLOOP' configuration " + "value to false. The app will now run without uvloop." + ) + + # uvloop requested and installed, but opted-out during install + elif strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")): + error_logger.warning( + "You are running the app using uvloop, but we've noticed " + "that the 'SANIC_NO_UVLOOP' environment variable " + "(used to opt-out of installing uvloop with Sanic) " + "is set to true. If you want to disable uvloop with Sanic, " + "set the 'USE_UVLOOP' configuration value to false." + ) self.router.ctx.app = self self.signal_router.ctx.app = self diff --git a/sanic/server/__init__.py b/sanic/server/__init__.py index 621ea957a1..d9caf5b9ec 100644 --- a/sanic/server/__init__.py +++ b/sanic/server/__init__.py @@ -6,7 +6,7 @@ from sanic.server.runners import serve, serve_multiple, serve_single -def use_uvloop(): +def use_uvloop() -> bool: """ Use uvloop (if available) instead of the default asyncio loop. @@ -19,7 +19,8 @@ def use_uvloop(): ): asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) except ImportError: - pass + return False + return True __all__ = ( From 93c814a32f870d1ded9b86052aff2e0a84d634ca Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sat, 9 Oct 2021 19:43:38 +0200 Subject: [PATCH 06/56] Tweaked warning message --- sanic/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sanic/app.py b/sanic/app.py index 26379f6a78..a433ad918a 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -218,7 +218,8 @@ def __init__( "installed in your system. In order to use uvloop " "you must first install it. Otherwise, you can disable " "uvloop completely by setting the 'USE_UVLOOP' configuration " - "value to false. The app will now run without uvloop." + "value to false. The app will now continue to run without " + "using uvloop." ) # uvloop requested and installed, but opted-out during install From d00b868b4092d306faa3e9561ee618b98c8e5f80 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sat, 9 Oct 2021 19:48:09 +0200 Subject: [PATCH 07/56] Fix linting --- sanic/app.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index a433ad918a..c3f41449c0 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -217,9 +217,9 @@ def __init__( "You are trying to use uvloop, but uvloop is not " "installed in your system. In order to use uvloop " "you must first install it. Otherwise, you can disable " - "uvloop completely by setting the 'USE_UVLOOP' configuration " - "value to false. The app will now continue to run without " - "using uvloop." + "uvloop completely by setting the 'USE_UVLOOP' " + "configuration value to false. The app will now continue " + "to run without using uvloop." ) # uvloop requested and installed, but opted-out during install @@ -228,8 +228,8 @@ def __init__( "You are running the app using uvloop, but we've noticed " "that the 'SANIC_NO_UVLOOP' environment variable " "(used to opt-out of installing uvloop with Sanic) " - "is set to true. If you want to disable uvloop with Sanic, " - "set the 'USE_UVLOOP' configuration value to false." + "is set to true. If you want to disable uvloop with " + "Sanic, set the 'USE_UVLOOP' configuration value to false." ) self.router.ctx.app = self From 48a54f81e59ea1e891666de825fd8ae962ce9d82 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sat, 9 Oct 2021 19:51:50 +0200 Subject: [PATCH 08/56] Better message --- sanic/app.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index c3f41449c0..0fbbbc0eca 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -225,11 +225,11 @@ def __init__( # uvloop requested and installed, but opted-out during install elif strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")): error_logger.warning( - "You are running the app using uvloop, but we've noticed " - "that the 'SANIC_NO_UVLOOP' environment variable " - "(used to opt-out of installing uvloop with Sanic) " - "is set to true. If you want to disable uvloop with " - "Sanic, set the 'USE_UVLOOP' configuration value to false." + "You are running the app using uvloop, but the " + "'SANIC_NO_UVLOOP' environment variable (used to opt-out " + "of installing uvloop with Sanic) is set to true. If you " + "want to disable uvloop with Sanic, set the 'USE_UVLOOP' " + "configuration value to false." ) self.router.ctx.app = self From 9576055b0534a404bf00aa2c34b92c6f23511f8d Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sat, 9 Oct 2021 20:39:21 +0200 Subject: [PATCH 09/56] Added tests --- tests/test_app.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/test_app.py b/tests/test_app.py index f222fba1a8..65eb79c161 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -2,13 +2,19 @@ import logging import re +from distutils.util import strtobool from inspect import isawaitable from os import environ from unittest.mock import Mock, patch import pytest +try: + import uvloop # noqa +except ImportError: + pass from sanic import Sanic +from sanic.compat import OS_IS_WINDOWS from sanic.config import Config from sanic.exceptions import SanicException from sanic.response import text @@ -444,3 +450,46 @@ class CustomContext: app = Sanic("custom", ctx=ctx) assert app.ctx == ctx + + +def test_app_uvloop_config(caplog): + if uvloop_installed(): + environ["SANIC_USE_UVLOOP"] = "false" + Sanic("dont_use_uvloop") + assert not isinstance( + asyncio.get_event_loop_policy(), + uvloop.EventLoopPolicy + ) + del environ["SANIC_USE_UVLOOP"] + + Sanic("use_uvloop") + assert isinstance( + asyncio.get_event_loop_policy(), + uvloop.EventLoopPolicy + ) + + environ["SANIC_NO_UVLOOP"] = "true" + with caplog.at_level(logging.WARNING): + Sanic("use_uvloop_with_no_uvloop_set") + + assert caplog.records[0].message == ( + "You are running the app using uvloop, but the " + "'SANIC_NO_UVLOOP' environment variable (used to opt-out " + "of installing uvloop with Sanic) is set to true. If you " + "want to disable uvloop with Sanic, set the 'USE_UVLOOP' " + "configuration value to false." + ) + del environ["SANIC_NO_UVLOOP"] + + elif not OS_IS_WINDOWS: + with caplog.at_level(logging.WARNING): + Sanic("wants_but_cant_use_uvloop") + + assert caplog.records[0].message == ( + "You are trying to use uvloop, but uvloop is not " + "installed in your system. In order to use uvloop " + "you must first install it. Otherwise, you can disable " + "uvloop completely by setting the 'USE_UVLOOP' " + "configuration value to false. The app will now continue " + "to run without using uvloop." + ) From 33eed1144eae0357d122fd7c79a471a8e2dc49d7 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sun, 10 Oct 2021 19:23:43 +0200 Subject: [PATCH 10/56] Remove previous tests --- tests/test_app.py | 43 ------------------------------------------- 1 file changed, 43 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 65eb79c161..75c576d047 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -450,46 +450,3 @@ class CustomContext: app = Sanic("custom", ctx=ctx) assert app.ctx == ctx - - -def test_app_uvloop_config(caplog): - if uvloop_installed(): - environ["SANIC_USE_UVLOOP"] = "false" - Sanic("dont_use_uvloop") - assert not isinstance( - asyncio.get_event_loop_policy(), - uvloop.EventLoopPolicy - ) - del environ["SANIC_USE_UVLOOP"] - - Sanic("use_uvloop") - assert isinstance( - asyncio.get_event_loop_policy(), - uvloop.EventLoopPolicy - ) - - environ["SANIC_NO_UVLOOP"] = "true" - with caplog.at_level(logging.WARNING): - Sanic("use_uvloop_with_no_uvloop_set") - - assert caplog.records[0].message == ( - "You are running the app using uvloop, but the " - "'SANIC_NO_UVLOOP' environment variable (used to opt-out " - "of installing uvloop with Sanic) is set to true. If you " - "want to disable uvloop with Sanic, set the 'USE_UVLOOP' " - "configuration value to false." - ) - del environ["SANIC_NO_UVLOOP"] - - elif not OS_IS_WINDOWS: - with caplog.at_level(logging.WARNING): - Sanic("wants_but_cant_use_uvloop") - - assert caplog.records[0].message == ( - "You are trying to use uvloop, but uvloop is not " - "installed in your system. In order to use uvloop " - "you must first install it. Otherwise, you can disable " - "uvloop completely by setting the 'USE_UVLOOP' " - "configuration value to false. The app will now continue " - "to run without using uvloop." - ) From 7f69e437431b4343de1d462f284d28415baaff40 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sun, 10 Oct 2021 19:38:35 +0200 Subject: [PATCH 11/56] Run event loop configuration on startup --- sanic/app.py | 51 +++++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 0fbbbc0eca..1902a5e59d 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -208,30 +208,6 @@ def __init__( if self.config.REGISTER: self.__class__.register_app(self) - if self.config.USE_UVLOOP and not OS_IS_WINDOWS: - uvloop_success = use_uvloop() - - # uvloop requested, but not installed - if not uvloop_success: - error_logger.warning( - "You are trying to use uvloop, but uvloop is not " - "installed in your system. In order to use uvloop " - "you must first install it. Otherwise, you can disable " - "uvloop completely by setting the 'USE_UVLOOP' " - "configuration value to false. The app will now continue " - "to run without using uvloop." - ) - - # uvloop requested and installed, but opted-out during install - elif strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")): - error_logger.warning( - "You are running the app using uvloop, but the " - "'SANIC_NO_UVLOOP' environment variable (used to opt-out " - "of installing uvloop with Sanic) is set to true. If you " - "want to disable uvloop with Sanic, set the 'USE_UVLOOP' " - "configuration value to false." - ) - self.router.ctx.app = self self.signal_router.ctx.app = self @@ -1034,6 +1010,8 @@ def run( "#asynchronous-support" ) + self._configure_event_loop() + if auto_reload or auto_reload is None and debug: self.auto_reload = True if os.environ.get("SANIC_SERVER_RUNNING") != "true": @@ -1155,6 +1133,8 @@ async def create_server( protocol = ( WebSocketProtocol if self.websocket_enabled else HttpProtocol ) + + self._configure_event_loop() # if access_log is passed explicitly change config.ACCESS_LOG if access_log is not None: self.config.ACCESS_LOG = access_log @@ -1438,6 +1418,29 @@ def update_config(self, config: Union[bytes, str, dict, Any]): self.config.update_config(config) + def _configure_event_loop(self): + if self.config.USE_UVLOOP and not OS_IS_WINDOWS: + uvloop_success = use_uvloop() + + if uvloop_success is False: + error_logger.warning( + "You are trying to use uvloop, but uvloop is not " + "installed in your system. In order to use uvloop " + "you must first install it. Otherwise, you can disable " + "uvloop completely by setting the 'USE_UVLOOP' " + "configuration value to false. Sanic will now continue " + "to run without using uvloop." + ) + + elif strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")): + error_logger.warning( + "You are running Sanic using uvloop, but the " + "'SANIC_NO_UVLOOP' environment variable (used to opt-out " + "of installing uvloop with Sanic) is set to true. If you " + "want to disable uvloop with Sanic, set the 'USE_UVLOOP' " + "configuration value to false." + ) + # -------------------------------------------------------------------- # # Class methods # -------------------------------------------------------------------- # From 257b239cacd7640835427005ef19ea85652ac2fe Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sun, 10 Oct 2021 20:46:57 +0200 Subject: [PATCH 12/56] Configure event loop using ASGI mode too --- sanic/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sanic/app.py b/sanic/app.py index 1902a5e59d..768e021f04 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1398,6 +1398,7 @@ async def __call__(self, scope, receive, send): details: https://asgi.readthedocs.io/en/latest """ self.asgi = True + self._configure_event_loop() self._asgi_app = await ASGIApp.create(self, scope, receive, send) asgi_app = self._asgi_app await asgi_app() From 4bfc09f3bdedcaa85ba49565856ec3031a3ffdd2 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Mon, 11 Oct 2021 19:00:43 +0200 Subject: [PATCH 13/56] Default `USE_UVLOOP` to `True` --- sanic/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/config.py b/sanic/config.py index 847815abce..3a7c280ab0 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -36,7 +36,7 @@ "REQUEST_MAX_SIZE": 100000000, # 100 megabytes "REQUEST_TIMEOUT": 60, # 60 seconds "RESPONSE_TIMEOUT": 60, # 60 seconds - "USE_UVLOOP": False, + "USE_UVLOOP": True, "WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte "WEBSOCKET_PING_INTERVAL": 20, "WEBSOCKET_PING_TIMEOUT": 20, From cab4d59fefe1d13b97f7a178fb44d21393de24cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9stor=20P=C3=A9rez?= <25409753+prryplatypus@users.noreply.github.com> Date: Mon, 11 Oct 2021 20:02:00 +0200 Subject: [PATCH 14/56] First attempt tests (#1) --- tests/test_app.py | 76 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/tests/test_app.py b/tests/test_app.py index 75c576d047..9140de1d96 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -450,3 +450,79 @@ class CustomContext: app = Sanic("custom", ctx=ctx) assert app.ctx == ctx + + +def test_app_uvloop_config(caplog): + if uvloop_installed(): + asyncio.set_event_loop_policy(None) + environ["SANIC_USE_UVLOOP"] = "false" + + app = Sanic("dont_use_uvloop") + + @app.get("/") + def _(request): + assert not isinstance( + asyncio.get_event_loop_policy(), + uvloop.EventLoopPolicy + ) + return text("ok") + + app.test_client.get("/") + del environ["SANIC_USE_UVLOOP"] + + app = Sanic("use_uvloop") + + @app.get("/") + async def _(request): + assert isinstance( + asyncio.get_event_loop_policy(), + uvloop.EventLoopPolicy + ) + return text("ok") + + app.test_client.get("/") + + asyncio.set_event_loop_policy(None) + + environ["SANIC_NO_UVLOOP"] = "true" + app = Sanic("use_uvloop_with_no_uvloop_set") + + with caplog.at_level(logging.WARNING): + @app.get("/") + async def _(request): + assert isinstance( + asyncio.get_event_loop_policy(), + uvloop.EventLoopPolicy + ) + return text("ok") + + app.test_client.get("/") + + del environ["SANIC_NO_UVLOOP"] + asyncio.set_event_loop_policy(None) + + assert caplog.records[0].message == ( + "You are running the app using uvloop, but the " + "'SANIC_NO_UVLOOP' environment variable (used to opt-out " + "of installing uvloop with Sanic) is set to true. If you " + "want to disable uvloop with Sanic, set the 'USE_UVLOOP' " + "configuration value to false." + ) + + elif not OS_IS_WINDOWS: + app = Sanic("wants_but_cant_use_uvloop") + + with caplog.at_level(logging.WARNING): + @app.get("/") + async def _(request): + return text("ok") + app.test_client.get("/") + + assert caplog.records[0].message == ( + "You are trying to use uvloop, but uvloop is not " + "installed in your system. In order to use uvloop " + "you must first install it. Otherwise, you can disable " + "uvloop completely by setting the 'USE_UVLOOP' " + "configuration value to false. The app will now continue " + "to run without using uvloop." + ) \ No newline at end of file From 0ab7f5ff6bfb7df7aea08e6024d306f169e3bf1c Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Mon, 11 Oct 2021 20:23:28 +0200 Subject: [PATCH 15/56] Attempt #2 at tests --- tests/test_app.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 9140de1d96..37a7e76347 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -2,7 +2,6 @@ import logging import re -from distutils.util import strtobool from inspect import isawaitable from os import environ from unittest.mock import Mock, patch @@ -501,13 +500,15 @@ async def _(request): del environ["SANIC_NO_UVLOOP"] asyncio.set_event_loop_policy(None) - assert caplog.records[0].message == ( + assert ( + "sanic.error", + logging.WARNING, "You are running the app using uvloop, but the " "'SANIC_NO_UVLOOP' environment variable (used to opt-out " "of installing uvloop with Sanic) is set to true. If you " "want to disable uvloop with Sanic, set the 'USE_UVLOOP' " "configuration value to false." - ) + ) in caplog.record_tuples elif not OS_IS_WINDOWS: app = Sanic("wants_but_cant_use_uvloop") @@ -518,11 +519,13 @@ async def _(request): return text("ok") app.test_client.get("/") - assert caplog.records[0].message == ( + assert ( + "sanic.error", + logging.WARNING, "You are trying to use uvloop, but uvloop is not " "installed in your system. In order to use uvloop " "you must first install it. Otherwise, you can disable " "uvloop completely by setting the 'USE_UVLOOP' " "configuration value to false. The app will now continue " "to run without using uvloop." - ) \ No newline at end of file + ) in caplog.record_tuples From cb791aa8b7a26f53431f7ca679ec218ad923556b Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Mon, 11 Oct 2021 20:34:34 +0200 Subject: [PATCH 16/56] Fix error message --- tests/test_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_app.py b/tests/test_app.py index 37a7e76347..9d3e30061c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -526,6 +526,6 @@ async def _(request): "installed in your system. In order to use uvloop " "you must first install it. Otherwise, you can disable " "uvloop completely by setting the 'USE_UVLOOP' " - "configuration value to false. The app will now continue " + "configuration value to false. Sanic will now continue " "to run without using uvloop." ) in caplog.record_tuples From eb2a2644048537929aaae7eb0f94ed6e56153e80 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Mon, 11 Oct 2021 20:57:26 +0200 Subject: [PATCH 17/56] Remove failed tests --- tests/test_app.py | 85 ----------------------------------------------- 1 file changed, 85 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 9d3e30061c..f222fba1a8 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -7,13 +7,8 @@ from unittest.mock import Mock, patch import pytest -try: - import uvloop # noqa -except ImportError: - pass from sanic import Sanic -from sanic.compat import OS_IS_WINDOWS from sanic.config import Config from sanic.exceptions import SanicException from sanic.response import text @@ -449,83 +444,3 @@ class CustomContext: app = Sanic("custom", ctx=ctx) assert app.ctx == ctx - - -def test_app_uvloop_config(caplog): - if uvloop_installed(): - asyncio.set_event_loop_policy(None) - environ["SANIC_USE_UVLOOP"] = "false" - - app = Sanic("dont_use_uvloop") - - @app.get("/") - def _(request): - assert not isinstance( - asyncio.get_event_loop_policy(), - uvloop.EventLoopPolicy - ) - return text("ok") - - app.test_client.get("/") - del environ["SANIC_USE_UVLOOP"] - - app = Sanic("use_uvloop") - - @app.get("/") - async def _(request): - assert isinstance( - asyncio.get_event_loop_policy(), - uvloop.EventLoopPolicy - ) - return text("ok") - - app.test_client.get("/") - - asyncio.set_event_loop_policy(None) - - environ["SANIC_NO_UVLOOP"] = "true" - app = Sanic("use_uvloop_with_no_uvloop_set") - - with caplog.at_level(logging.WARNING): - @app.get("/") - async def _(request): - assert isinstance( - asyncio.get_event_loop_policy(), - uvloop.EventLoopPolicy - ) - return text("ok") - - app.test_client.get("/") - - del environ["SANIC_NO_UVLOOP"] - asyncio.set_event_loop_policy(None) - - assert ( - "sanic.error", - logging.WARNING, - "You are running the app using uvloop, but the " - "'SANIC_NO_UVLOOP' environment variable (used to opt-out " - "of installing uvloop with Sanic) is set to true. If you " - "want to disable uvloop with Sanic, set the 'USE_UVLOOP' " - "configuration value to false." - ) in caplog.record_tuples - - elif not OS_IS_WINDOWS: - app = Sanic("wants_but_cant_use_uvloop") - - with caplog.at_level(logging.WARNING): - @app.get("/") - async def _(request): - return text("ok") - app.test_client.get("/") - - assert ( - "sanic.error", - logging.WARNING, - "You are trying to use uvloop, but uvloop is not " - "installed in your system. In order to use uvloop " - "you must first install it. Otherwise, you can disable " - "uvloop completely by setting the 'USE_UVLOOP' " - "configuration value to false. Sanic will now continue " - "to run without using uvloop." - ) in caplog.record_tuples From 9a9a1ea5d6a8dc0b7619e628eb99ba8aa826fa12 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Mon, 18 Oct 2021 21:33:05 +0200 Subject: [PATCH 18/56] Reattempt testing number 1 --- tests/test_app.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/test_app.py b/tests/test_app.py index f222fba1a8..59463cfb11 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -8,7 +8,10 @@ import pytest +import sanic.app + from sanic import Sanic +from sanic.compat import OS_IS_WINDOWS from sanic.config import Config from sanic.exceptions import SanicException from sanic.response import text @@ -444,3 +447,35 @@ class CustomContext: app = Sanic("custom", ctx=ctx) assert app.ctx == ctx + + +def test_uvloop_config_enabled(monkeypatch): + app = Sanic("uvloop") + + err_logger = Mock() + monkeypatch.setattr(sanic.app, "error_logger", err_logger) + + use_uvloop = Mock(return_value=uvloop_installed()) + monkeypatch.setattr(sanic.app, "use_uvloop", use_uvloop) + + @app.get("/1") + def _(request): + if OS_IS_WINDOWS: + use_uvloop.assert_not_called() + return text("test") + + use_uvloop.assert_called_once() + + if not uvloop_installed(): + err_logger.assert_called_with( + "You are trying to use uvloop, but uvloop is not " + "installed in your system. In order to use uvloop " + "you must first install it. Otherwise, you can disable " + "uvloop completely by setting the 'USE_UVLOOP' " + "configuration value to false. Sanic will now continue " + "to run without using uvloop." + ) + + return text("test") + + app.test_client.get("/1") From 2e492f94e6b78532c417aada015a0e83371ec779 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sat, 23 Oct 2021 20:39:57 +0200 Subject: [PATCH 19/56] Create `UVLOOP_INSTALLED` constant --- sanic/app.py | 11 +++++------ sanic/compat.py | 7 +++++++ sanic/server/__init__.py | 17 ++++++----------- tests/test_app.py | 16 ++++------------ 4 files changed, 22 insertions(+), 29 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 768e021f04..e122a3f983 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -50,7 +50,7 @@ from sanic.base import BaseSanic from sanic.blueprint_group import BlueprintGroup from sanic.blueprints import Blueprint -from sanic.compat import OS_IS_WINDOWS +from sanic.compat import OS_IS_WINDOWS, UVLOOP_INSTALLED from sanic.config import BASE_LOGO, SANIC_PREFIX, Config from sanic.exceptions import ( InvalidUsage, @@ -1421,10 +1421,8 @@ def update_config(self, config: Union[bytes, str, dict, Any]): def _configure_event_loop(self): if self.config.USE_UVLOOP and not OS_IS_WINDOWS: - uvloop_success = use_uvloop() - - if uvloop_success is False: - error_logger.warning( + if not UVLOOP_INSTALLED: + return error_logger.warning( "You are trying to use uvloop, but uvloop is not " "installed in your system. In order to use uvloop " "you must first install it. Otherwise, you can disable " @@ -1433,7 +1431,8 @@ def _configure_event_loop(self): "to run without using uvloop." ) - elif strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")): + use_uvloop() + if strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")): error_logger.warning( "You are running Sanic using uvloop, but the " "'SANIC_NO_UVLOOP' environment variable (used to opt-out " diff --git a/sanic/compat.py b/sanic/compat.py index f8b3a74ae9..fe0971f7d2 100644 --- a/sanic/compat.py +++ b/sanic/compat.py @@ -8,6 +8,13 @@ OS_IS_WINDOWS = os.name == "nt" +UVLOOP_INSTALLED = False + +try: + import uvloop # type: ignore # noqa + UVLOOP_INSTALLED = True +except ImportError: + pass class Header(CIMultiDict): diff --git a/sanic/server/__init__.py b/sanic/server/__init__.py index d9caf5b9ec..1c63690e86 100644 --- a/sanic/server/__init__.py +++ b/sanic/server/__init__.py @@ -8,19 +8,14 @@ def use_uvloop() -> bool: """ - Use uvloop (if available) instead of the default - asyncio loop. + Use uvloop instead of the default asyncio loop. """ - try: - import uvloop # type: ignore + import uvloop # type: ignore - if not isinstance( - asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy - ): - asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) - except ImportError: - return False - return True + if not isinstance( + asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy + ): + asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) __all__ = ( diff --git a/tests/test_app.py b/tests/test_app.py index f222fba1a8..88c37e2fa6 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -9,6 +9,7 @@ import pytest from sanic import Sanic +from sanic.compat import UVLOOP_INSTALLED from sanic.config import Config from sanic.exceptions import SanicException from sanic.response import text @@ -19,15 +20,6 @@ def clear_app_registry(): Sanic._app_registry = {} -def uvloop_installed(): - try: - import uvloop # noqa - - return True - except ImportError: - return False - - def test_app_loop_running(app): @app.get("/test") async def handler(request): @@ -39,7 +31,7 @@ async def handler(request): def test_create_asyncio_server(app): - if not uvloop_installed(): + if not UVLOOP_INSTALLED: loop = asyncio.get_event_loop() asyncio_srv_coro = app.create_server(return_asyncio_server=True) assert isawaitable(asyncio_srv_coro) @@ -48,7 +40,7 @@ def test_create_asyncio_server(app): def test_asyncio_server_no_start_serving(app): - if not uvloop_installed(): + if not UVLOOP_INSTALLED: loop = asyncio.get_event_loop() asyncio_srv_coro = app.create_server( port=43123, @@ -60,7 +52,7 @@ def test_asyncio_server_no_start_serving(app): def test_asyncio_server_start_serving(app): - if not uvloop_installed(): + if not UVLOOP_INSTALLED: loop = asyncio.get_event_loop() asyncio_srv_coro = app.create_server( port=43124, From 2ff45ccfb196d6ea8417b2e3ee11561f04074389 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sat, 23 Oct 2021 20:50:13 +0200 Subject: [PATCH 20/56] Fix typing --- sanic/server/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/server/__init__.py b/sanic/server/__init__.py index 1c63690e86..8c915b66c2 100644 --- a/sanic/server/__init__.py +++ b/sanic/server/__init__.py @@ -6,7 +6,7 @@ from sanic.server.runners import serve, serve_multiple, serve_single -def use_uvloop() -> bool: +def use_uvloop() -> None: """ Use uvloop instead of the default asyncio loop. """ From 1908eb5a021a4a5c6da71285424060190280e813 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sat, 23 Oct 2021 21:02:03 +0200 Subject: [PATCH 21/56] Fix linting... *sigh* --- sanic/compat.py | 1 + sanic/server/__init__.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/sanic/compat.py b/sanic/compat.py index fe0971f7d2..24e4b03799 100644 --- a/sanic/compat.py +++ b/sanic/compat.py @@ -12,6 +12,7 @@ try: import uvloop # type: ignore # noqa + UVLOOP_INSTALLED = True except ImportError: pass diff --git a/sanic/server/__init__.py b/sanic/server/__init__.py index 8c915b66c2..0846836fcf 100644 --- a/sanic/server/__init__.py +++ b/sanic/server/__init__.py @@ -12,9 +12,7 @@ def use_uvloop() -> None: """ import uvloop # type: ignore - if not isinstance( - asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy - ): + if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy): asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) From 6563ee28bfa61f66040de2d550406ee176f26d94 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sat, 23 Oct 2021 21:16:49 +0200 Subject: [PATCH 22/56] Fix tests (hopefully?) --- tests/test_exceptions.py | 18 ++++++------------ tests/test_exceptions_handler.py | 6 ++++-- tests/test_graceful_shutdown.py | 6 +++--- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 503e47cbb1..041766a5d6 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -4,7 +4,6 @@ import pytest from bs4 import BeautifulSoup -from websockets.version import version as websockets_version from sanic import Sanic from sanic.exceptions import ( @@ -260,15 +259,10 @@ async def feed(request, ws): raise Exception("...") with caplog.at_level(logging.INFO): - app.test_client.websocket("/feed") - # Websockets v10.0 and above output an additional - # INFO message when a ws connection is accepted - ws_version_parts = websockets_version.split(".") - ws_major = int(ws_version_parts[0]) - record_index = 2 if ws_major >= 10 else 1 - assert caplog.record_tuples[record_index][0] == "sanic.error" - assert caplog.record_tuples[record_index][1] == logging.ERROR + req, _ = app.test_client.websocket("/feed") + assert ( - "Exception occurred while handling uri:" - in caplog.record_tuples[record_index][2] - ) + "sanic.error", + logging.ERROR, + "Exception occurred while handling uri: %s" % repr(req.url) + ) in caplog.record_tuples diff --git a/tests/test_exceptions_handler.py b/tests/test_exceptions_handler.py index 9bedf7e67c..e5b8fb8c09 100644 --- a/tests/test_exceptions_handler.py +++ b/tests/test_exceptions_handler.py @@ -219,11 +219,13 @@ def lookup(self, exception): with caplog.at_level(logging.WARNING): _, response = exception_handler_app.test_client.get("/1") - assert caplog.records[0].message == ( + assert ( + "sanic.error", + logging.WARNING, "You are using a deprecated error handler. The lookup method should " "accept two positional parameters: (exception, route_name: " "Optional[str]). Until you upgrade your ErrorHandler.lookup, " "Blueprint specific exceptions will not work properly. Beginning in " "v22.3, the legacy style lookup method will not work at all." - ) + ) in caplog.record_tuples assert response.status == 400 diff --git a/tests/test_graceful_shutdown.py b/tests/test_graceful_shutdown.py index 8380ed50d2..a2e3bb796e 100644 --- a/tests/test_graceful_shutdown.py +++ b/tests/test_graceful_shutdown.py @@ -40,7 +40,7 @@ def ping(): assert counter[logging.INFO] == 5 assert logging.ERROR not in counter - assert ( - caplog.record_tuples[3][2] - == "Request: GET http://127.0.0.1:8000/ stopped. Transport is closed." + assert any( + r[2] == "Request: GET http://127.0.0.1:8000/ stopped. Transport is closed." + for r in caplog.record_tuples ) From a1f36609b4d7ea23acd33c08ac5781a215483cee Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sat, 23 Oct 2021 21:40:57 +0200 Subject: [PATCH 23/56] Fix logging --- tests/test_logging.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_logging.py b/tests/test_logging.py index 639bb2ee6f..33b0dcdf01 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -187,17 +187,17 @@ def log_info(request): port = test_client.port - assert caplog.record_tuples[0] == ( + assert ( "sanic.root", logging.INFO, f"Goin' Fast @ https://127.0.0.1:{port}", - ) - assert caplog.record_tuples[1] == ( + ) in caplog.record_tuples + assert ( "sanic.root", logging.INFO, f"https://127.0.0.1:{port}/", - ) - assert caplog.record_tuples[2] == ("sanic.root", logging.INFO, rand_string) + ) in caplog.record_tuples + assert ("sanic.root", logging.INFO, rand_string) in caplog.record_tuples assert caplog.record_tuples[-1] == ( "sanic.root", logging.INFO, From 0d1e591a9be7a592338c1e8a0d3958cac7bda2c7 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sat, 23 Oct 2021 21:42:46 +0200 Subject: [PATCH 24/56] Fix logo tests? --- sanic/app.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index e122a3f983..ca7e6a7d1f 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1010,8 +1010,6 @@ def run( "#asynchronous-support" ) - self._configure_event_loop() - if auto_reload or auto_reload is None and debug: self.auto_reload = True if os.environ.get("SANIC_SERVER_RUNNING") != "true": @@ -1134,7 +1132,6 @@ async def create_server( WebSocketProtocol if self.websocket_enabled else HttpProtocol ) - self._configure_event_loop() # if access_log is passed explicitly change config.ACCESS_LOG if access_log is not None: self.config.ACCESS_LOG = access_log @@ -1146,7 +1143,7 @@ async def create_server( ssl=ssl, sock=sock, unix=unix, - loop=get_event_loop(), + create_loop=True, protocol=protocol, backlog=backlog, run_async=return_asyncio_server, @@ -1256,7 +1253,7 @@ def _helper( sock=None, unix=None, workers=1, - loop=None, + create_loop=None, protocol=HttpProtocol, backlog=100, register_sys_signals=True, @@ -1293,7 +1290,7 @@ def _helper( "ssl": ssl, "app": self, "signal": ServerSignal(), - "loop": loop, + "loop": None, "register_sys_signals": register_sys_signals, "backlog": backlog, } @@ -1324,6 +1321,10 @@ def _helper( else BASE_LOGO ) + self._configure_event_loop() + if create_loop: + server_settings["loop"] = get_event_loop() + if run_async: server_settings["run_async"] = True From c008c343d95fc170893452670a4efafc9c844060 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sun, 24 Oct 2021 19:27:43 +0200 Subject: [PATCH 25/56] Rename loop param --- sanic/app.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index e122a3f983..28461ac731 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1146,7 +1146,7 @@ async def create_server( ssl=ssl, sock=sock, unix=unix, - loop=get_event_loop(), + use_existing_loop=True, protocol=protocol, backlog=backlog, run_async=return_asyncio_server, @@ -1256,7 +1256,7 @@ def _helper( sock=None, unix=None, workers=1, - loop=None, + use_existing_loop=False, protocol=HttpProtocol, backlog=100, register_sys_signals=True, @@ -1293,7 +1293,7 @@ def _helper( "ssl": ssl, "app": self, "signal": ServerSignal(), - "loop": loop, + "loop": None, "register_sys_signals": register_sys_signals, "backlog": backlog, } @@ -1324,6 +1324,10 @@ def _helper( else BASE_LOGO ) + self._configure_event_loop() + if use_existing_loop: + server_settings["loop"] = get_event_loop() + if run_async: server_settings["run_async"] = True From 41454b255376ea3b54822e8d20f089916a80cc11 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sun, 24 Oct 2021 20:51:05 +0200 Subject: [PATCH 26/56] Fix linting errors --- sanic/app.py | 1 - sanic/server/__init__.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/app.py b/sanic/app.py index 5b64803ede..eb853a9d65 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -49,7 +49,6 @@ from sanic.base import BaseSanic from sanic.blueprint_group import BlueprintGroup from sanic.blueprints import Blueprint -from sanic.compat import OS_IS_WINDOWS from sanic.config import BASE_LOGO, SANIC_PREFIX, Config from sanic.exceptions import ( InvalidUsage, diff --git a/sanic/server/__init__.py b/sanic/server/__init__.py index dda26bc5fe..f5e898e1ac 100644 --- a/sanic/server/__init__.py +++ b/sanic/server/__init__.py @@ -17,6 +17,7 @@ def use_uvloop() -> None: """ try: import uvloop # type: ignore + if strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")): error_logger.warning( "You are running Sanic using uvloop, but the " From 76492a7f886cefe33b0a2469a24ede18ef23022b Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sun, 24 Oct 2021 21:08:24 +0200 Subject: [PATCH 27/56] Avoid unnecessary error log --- sanic/worker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sanic/worker.py b/sanic/worker.py index 0df57eb07b..91c0f5903a 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -7,6 +7,7 @@ from gunicorn.workers import base # type: ignore +from sanic.compat import UVLOOP_INSTALLED from sanic.log import logger from sanic.server import HttpProtocol, Signal, serve, use_uvloop from sanic.server.protocols.websocket_protocol import WebSocketProtocol @@ -17,7 +18,8 @@ except ImportError: ssl = None # type: ignore -use_uvloop() +if UVLOOP_INSTALLED: + use_uvloop() class GunicornWorker(base.Worker): From 0dbf1b25041f1435261d5fb81d1ea202e6eff9c9 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Mon, 1 Nov 2021 19:29:58 +0100 Subject: [PATCH 28/56] Added first tests --- tests/test_app.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_app.py b/tests/test_app.py index 88c37e2fa6..04f63ca04f 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -7,6 +7,7 @@ from unittest.mock import Mock, patch import pytest +import sanic from sanic import Sanic from sanic.compat import UVLOOP_INSTALLED @@ -436,3 +437,34 @@ class CustomContext: app = Sanic("custom", ctx=ctx) assert app.ctx == ctx + + +def test_uvloop_usage(app, monkeypatch): + @app.get("/test") + def handler(request): + return text("ok") + + use_uvloop = Mock() + monkeypatch.setattr(sanic.app, "use_uvloop", use_uvloop) + + app.config["USE_UVLOOP"] = False + app.test_client.get("/test") + use_uvloop.assert_not_called() + + app.config["USE_UVLOOP"] = True + app.test_client.get("/test") + use_uvloop.assert_called_once() + + # Try with create_server + use_uvloop.reset_mock() + loop = asyncio.get_event_loop() + + app.config["USE_UVLOOP"] = False + asyncio_srv_coro = app.create_server(return_asyncio_server=True) + loop.run_until_complete(asyncio_srv_coro) + use_uvloop.assert_not_called() + + app.config["USE_UVLOOP"] = True + asyncio_srv_coro = app.create_server(return_asyncio_server=True) + loop.run_until_complete(asyncio_srv_coro) + use_uvloop.assert_called_once() From 9de0e341e96fc86ced7e7549fc50755c378d0a9c Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Mon, 1 Nov 2021 19:57:50 +0100 Subject: [PATCH 29/56] Split create_server tests so loop isn't closed prematurely --- tests/test_app.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 04f63ca04f..18d06b9c2c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -455,16 +455,29 @@ def handler(request): app.test_client.get("/test") use_uvloop.assert_called_once() - # Try with create_server - use_uvloop.reset_mock() + +def test_uvloop_usage_with_create_server(app, monkeypatch): + @app.get("/test") + def handler(request): + return text("ok") + + use_uvloop = Mock() + monkeypatch.setattr(sanic.app, "use_uvloop", use_uvloop) + loop = asyncio.get_event_loop() app.config["USE_UVLOOP"] = False - asyncio_srv_coro = app.create_server(return_asyncio_server=True) + asyncio_srv_coro = app.create_server( + return_asyncio_server=True, + asyncio_server_kwargs=dict(start_serving=False) + ) loop.run_until_complete(asyncio_srv_coro) use_uvloop.assert_not_called() app.config["USE_UVLOOP"] = True - asyncio_srv_coro = app.create_server(return_asyncio_server=True) + asyncio_srv_coro = app.create_server( + return_asyncio_server=True, + asyncio_server_kwargs=dict(start_serving=False) + ) loop.run_until_complete(asyncio_srv_coro) use_uvloop.assert_called_once() From e6a195b61ff6315f5c7c5ce60b22a28dc1753f13 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Mon, 1 Nov 2021 21:07:47 +0100 Subject: [PATCH 30/56] Move loop setup to after logo --- sanic/app.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 2c89499178..23b21ff658 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1034,9 +1034,6 @@ def run( WebSocketProtocol if self.websocket_enabled else HttpProtocol ) - if self.config.USE_UVLOOP: - use_uvloop() - # if access_log is passed explicitly change config.ACCESS_LOG if access_log is not None: self.config.ACCESS_LOG = access_log @@ -1154,9 +1151,6 @@ async def create_server( WebSocketProtocol if self.websocket_enabled else HttpProtocol ) - if self.config.USE_UVLOOP: - use_uvloop() - # if access_log is passed explicitly change config.ACCESS_LOG if access_log is not None: self.config.ACCESS_LOG = access_log @@ -1171,7 +1165,7 @@ async def create_server( ssl=ssl, sock=sock, unix=unix, - loop=get_event_loop(), + loop=True, protocol=protocol, backlog=backlog, run_async=return_asyncio_server, @@ -1281,7 +1275,7 @@ def _helper( sock=None, unix=None, workers=1, - loop=None, + loop: Optional[Union[bool, AbstractEventLoop]] = None, protocol=HttpProtocol, backlog=100, register_sys_signals=True, @@ -1309,6 +1303,14 @@ def _helper( if isinstance(self.config.LOGO, str) else BASE_LOGO ) + + if not isinstance(loop, AbstractEventLoop): + if self.config.USE_UVLOOP: + use_uvloop() + + if loop is True: + loop = get_event_loop() + # Serve if host and port: proto = "http" From 5ca22d7a40c84e373e214ea5fb8634e576b920f1 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Mon, 1 Nov 2021 21:21:02 +0100 Subject: [PATCH 31/56] Fix some tests --- tests/test_exceptions.py | 17 ++++++----------- tests/test_exceptions_handler.py | 6 ++++-- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 503e47cbb1..71d3c2c888 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -260,15 +260,10 @@ async def feed(request, ws): raise Exception("...") with caplog.at_level(logging.INFO): - app.test_client.websocket("/feed") - # Websockets v10.0 and above output an additional - # INFO message when a ws connection is accepted - ws_version_parts = websockets_version.split(".") - ws_major = int(ws_version_parts[0]) - record_index = 2 if ws_major >= 10 else 1 - assert caplog.record_tuples[record_index][0] == "sanic.error" - assert caplog.record_tuples[record_index][1] == logging.ERROR + req, _ = app.test_client.websocket("/feed") + assert ( - "Exception occurred while handling uri:" - in caplog.record_tuples[record_index][2] - ) + "sanic.error", + logging.ERROR, + "Exception occurred while handling uri: %s" % repr(req.url), + ) in caplog.record_tuples diff --git a/tests/test_exceptions_handler.py b/tests/test_exceptions_handler.py index edc5a32706..a953c4f256 100644 --- a/tests/test_exceptions_handler.py +++ b/tests/test_exceptions_handler.py @@ -220,13 +220,15 @@ def lookup(self, exception): with caplog.at_level(logging.WARNING): _, response = exception_handler_app.test_client.get("/1") - assert caplog.records[0].message == ( + assert ( + "sanic.error", + logging.WARNING, "You are using a deprecated error handler. The lookup method should " "accept two positional parameters: (exception, route_name: " "Optional[str]). Until you upgrade your ErrorHandler.lookup, " "Blueprint specific exceptions will not work properly. Beginning in " "v22.3, the legacy style lookup method will not work at all." - ) + ) in caplog.record_tuples assert response.status == 400 From 934d737a923fb8431dfdf91416be906afbee5626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9stor=20P=C3=A9rez?= <25409753+prryplatypus@users.noreply.github.com> Date: Tue, 2 Nov 2021 16:22:29 +0100 Subject: [PATCH 32/56] Move loop setup to after startup debug messages --- sanic/app.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 23b21ff658..59150d7300 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1304,13 +1304,6 @@ def _helper( else BASE_LOGO ) - if not isinstance(loop, AbstractEventLoop): - if self.config.USE_UVLOOP: - use_uvloop() - - if loop is True: - loop = get_event_loop() - # Serve if host and port: proto = "http" @@ -1327,6 +1320,13 @@ def _helper( reload_mode = "enabled" if auto_reload else "disabled" logger.debug(f"Sanic auto-reload: {reload_mode}") logger.debug(f"Sanic debug mode: {debug_mode}") + + if not isinstance(loop, AbstractEventLoop): + if self.config.USE_UVLOOP: + use_uvloop() + + if loop is True: + loop = get_event_loop() ssl = process_to_context(ssl) From 355a9e2daecfeb4d433ee5cc43c05209888f4392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9stor=20P=C3=A9rez?= <25409753+prryplatypus@users.noreply.github.com> Date: Sun, 7 Nov 2021 21:11:08 +0100 Subject: [PATCH 33/56] Fix broken test from merge --- tests/test_exceptions_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_exceptions_handler.py b/tests/test_exceptions_handler.py index e1e6765fe1..9ad595fc18 100644 --- a/tests/test_exceptions_handler.py +++ b/tests/test_exceptions_handler.py @@ -231,7 +231,7 @@ def lookup(self, exception): "Optional[str]). Until you upgrade your ErrorHandler.lookup, " "Blueprint specific exceptions will not work properly. Beginning in " "v22.3, the legacy style lookup method will not work at all." - ) in caplog.record_tuples + ) assert response.status == 400 From 2a35578e464323315b8a253e3a15dab5b7c6977b Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Thu, 18 Nov 2021 21:10:41 +0100 Subject: [PATCH 34/56] Fixed some test errors --- tests/test_exceptions.py | 10 +++++++--- tests/test_exceptions_handler.py | 2 +- tests/test_graceful_shutdown.py | 20 +++++++++++--------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index eea9793509..cfc0f83252 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -271,9 +271,13 @@ async def feed(request, ws): with caplog.at_level(logging.INFO): app.test_client.websocket("/feed") - error_logs = [r for r in caplog.record_tuples if r[0] == "sanic.error"] - assert error_logs[1][1] == logging.ERROR - assert "Exception occurred while handling uri:" in error_logs[1][2] + for record in caplog.record_tuples: + if record[2].startswith("Exception occurred"): + break + + assert record[0] == "sanic.error" + assert record[1] == logging.ERROR + assert "Exception occurred while handling uri:" in record[2] @pytest.mark.parametrize("debug", (True, False)) diff --git a/tests/test_exceptions_handler.py b/tests/test_exceptions_handler.py index 9ad595fc18..91411ce45e 100644 --- a/tests/test_exceptions_handler.py +++ b/tests/test_exceptions_handler.py @@ -222,7 +222,7 @@ def lookup(self, exception): _, response = exception_handler_app.test_client.get("/1") for record in caplog.records: - if record.message.startswith("You are"): + if record.message.startswith("You are using"): break assert record.message == ( diff --git a/tests/test_graceful_shutdown.py b/tests/test_graceful_shutdown.py index 1733ffd15b..54ba92d89a 100644 --- a/tests/test_graceful_shutdown.py +++ b/tests/test_graceful_shutdown.py @@ -2,7 +2,6 @@ import logging import time -from collections import Counter from multiprocessing import Process import httpx @@ -36,11 +35,14 @@ def ping(): p.kill() - counter = Counter([r[1] for r in caplog.record_tuples]) - - assert counter[logging.INFO] == 11 - assert logging.ERROR not in counter - assert ( - caplog.record_tuples[9][2] - == "Request: GET http://127.0.0.1:8000/ stopped. Transport is closed." - ) + info = 0 + for record in caplog.record_tuples: + assert record[1] != logging.ERROR + if record[1] == logging.INFO: + info += 1 + if record[2].startswith("Request:"): + assert record[2] == ( + "Request: GET http://127.0.0.1:8000/ stopped. " + "Transport is closed." + ) + assert info == 11 From 877f5e6aefca221b43cec94862e240d38e1ff2f5 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Mon, 6 Dec 2021 20:35:11 +0100 Subject: [PATCH 35/56] Only change loop policy in non-async methods --- sanic/app.py | 16 +++++----------- tests/test_app.py | 8 +++++--- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index da54d5ddfe..bb27d47a5b 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1102,6 +1102,9 @@ def run( register_sys_signals=register_sys_signals, ) + if self.config.USE_UVLOOP: + use_uvloop() + try: self.is_running = True self.is_stopping = False @@ -1213,7 +1216,7 @@ async def create_server( ssl=ssl, sock=sock, unix=unix, - loop=True, + loop=get_event_loop(), protocol=protocol, backlog=backlog, run_async=return_asyncio_server, @@ -1323,7 +1326,7 @@ def _helper( sock: Optional[socket] = None, unix: Optional[str] = None, workers: int = 1, - loop: Union[AbstractEventLoop, bool] = None, + loop: AbstractEventLoop = None, protocol: Type[Protocol] = HttpProtocol, backlog: int = 100, register_sys_signals: bool = True, @@ -1356,13 +1359,6 @@ def _helper( display_host = f"[{host}]" if ":" in host else host serve_location = f"{proto}://{display_host}:{port}" - if not isinstance(loop, AbstractEventLoop): - if self.config.USE_UVLOOP: - use_uvloop() - - if loop is True: - loop = get_event_loop() - ssl = process_to_context(ssl) server_settings = { @@ -1459,8 +1455,6 @@ async def __call__(self, scope, receive, send): details: https://asgi.readthedocs.io/en/latest """ self.asgi = True - if self.config.USE_UVLOOP: - use_uvloop() self.motd("") self._asgi_app = await ASGIApp.create(self, scope, receive, send) asgi_app = self._asgi_app diff --git a/tests/test_app.py b/tests/test_app.py index 35412d389a..ba244501e6 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -440,6 +440,7 @@ class CustomContext: assert app.ctx == ctx + def test_uvloop_usage(app, monkeypatch): @app.get("/test") def handler(request): @@ -457,7 +458,7 @@ def handler(request): use_uvloop.assert_called_once() -def test_uvloop_usage_with_create_server(app, monkeypatch): +def test_uvloop_is_ignored_with_create_server(app, monkeypatch): @app.get("/test") def handler(request): return text("ok") @@ -481,9 +482,10 @@ def handler(request): asyncio_server_kwargs=dict(start_serving=False) ) loop.run_until_complete(asyncio_srv_coro) - use_uvloop.assert_called_once() + use_uvloop.assert_not_called() + def test_cannot_run_fast_and_workers(app): message = "You cannot use both fast=True and workers=X" with pytest.raises(RuntimeError, match=message): - app.run(fast=True, workers=4) \ No newline at end of file + app.run(fast=True, workers=4) From 62b9d61fc0ea597e838b040064c07a2b5581fe33 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Tue, 7 Dec 2021 19:55:08 +0100 Subject: [PATCH 36/56] Move uvloop setup method to its own file --- sanic/app.py | 4 ++-- sanic/server/__init__.py | 42 ++------------------------------------ sanic/server/loop.py | 44 ++++++++++++++++++++++++++++++++++++++++ sanic/worker.py | 4 ++-- 4 files changed, 50 insertions(+), 44 deletions(-) create mode 100644 sanic/server/loop.py diff --git a/sanic/app.py b/sanic/app.py index bb27d47a5b..cbd3ed2fd3 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -84,7 +84,7 @@ from sanic.router import Router from sanic.server import AsyncioServer, HttpProtocol from sanic.server import Signal as ServerSignal -from sanic.server import serve, serve_multiple, serve_single, use_uvloop +from sanic.server import serve, serve_multiple, serve_single, try_use_uvloop from sanic.server.protocols.websocket_protocol import WebSocketProtocol from sanic.server.websockets.impl import ConnectionClosed from sanic.signals import Signal, SignalRouter @@ -1103,7 +1103,7 @@ def run( ) if self.config.USE_UVLOOP: - use_uvloop() + try_use_uvloop() try: self.is_running = True diff --git a/sanic/server/__init__.py b/sanic/server/__init__.py index f5e898e1ac..116bd05cf3 100644 --- a/sanic/server/__init__.py +++ b/sanic/server/__init__.py @@ -1,49 +1,10 @@ -import asyncio -import os - -from distutils.util import strtobool - -from sanic.compat import OS_IS_WINDOWS -from sanic.log import error_logger from sanic.models.server_types import ConnInfo, Signal from sanic.server.async_server import AsyncioServer +from sanic.server.loop import try_use_uvloop from sanic.server.protocols.http_protocol import HttpProtocol from sanic.server.runners import serve, serve_multiple, serve_single -def use_uvloop() -> None: - """ - Use uvloop instead of the default asyncio loop. - """ - try: - import uvloop # type: ignore - - if strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")): - error_logger.warning( - "You are running Sanic using uvloop, but the " - "'SANIC_NO_UVLOOP' environment variable (used to opt-out " - "of installing uvloop with Sanic) is set to true. If you " - "want to disable uvloop with Sanic, set the 'USE_UVLOOP' " - "configuration value to false." - ) - - if not isinstance( - asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy - ): - asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) - - except ImportError: - if not OS_IS_WINDOWS: - error_logger.warning( - "You are trying to use uvloop, but uvloop is not " - "installed in your system. In order to use uvloop " - "you must first install it. Otherwise, you can disable " - "uvloop completely by setting the 'USE_UVLOOP' " - "configuration value to false. Sanic will now continue " - "to run without using uvloop." - ) - - __all__ = ( "AsyncioServer", "ConnInfo", @@ -52,4 +13,5 @@ def use_uvloop() -> None: "serve", "serve_multiple", "serve_single", + "try_use_uvloop", ) diff --git a/sanic/server/loop.py b/sanic/server/loop.py new file mode 100644 index 0000000000..b6283727b9 --- /dev/null +++ b/sanic/server/loop.py @@ -0,0 +1,44 @@ +import asyncio +import os + +from distutils.util import strtobool + +from sanic.compat import OS_IS_WINDOWS +from sanic.log import error_logger + + +def try_use_uvloop() -> None: + """ + Use uvloop instead of the default asyncio loop. + """ + if OS_IS_WINDOWS: # uvloop is not compatible + return + + try: + import uvloop # type: ignore + except ImportError: + error_logger.warning( + "You are trying to use uvloop, but uvloop is not " + "installed in your system. In order to use uvloop " + "you must first install it. Otherwise, you can disable " + "uvloop completely by setting the 'USE_UVLOOP' " + "configuration value to false. Sanic will now continue " + "to run with the default event loop." + ) + return + + uvloop_install_removed = strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")) + if uvloop_install_removed: + error_logger.info( + "You are requesting to run Sanic using uvloop, but the " + "install-time 'SANIC_NO_UVLOOP' environment variable (used to " + "opt-out of installing uvloop with Sanic) is set to true. If " + "you want to prevent Sanic from overriding the event loop policy " + "during runtime, set the 'USE_UVLOOP' configuration value to " + "false." + ) + + if not isinstance( + asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy + ): + asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) diff --git a/sanic/worker.py b/sanic/worker.py index 91c0f5903a..cdc238e26e 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -9,7 +9,7 @@ from sanic.compat import UVLOOP_INSTALLED from sanic.log import logger -from sanic.server import HttpProtocol, Signal, serve, use_uvloop +from sanic.server import HttpProtocol, Signal, serve, try_use_uvloop from sanic.server.protocols.websocket_protocol import WebSocketProtocol @@ -19,7 +19,7 @@ ssl = None # type: ignore if UVLOOP_INSTALLED: - use_uvloop() + try_use_uvloop() class GunicornWorker(base.Worker): From b1b9db33ce849d197566e059d6cebc17ae8271d7 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Tue, 7 Dec 2021 20:14:16 +0100 Subject: [PATCH 37/56] Distinguish between explicit and default config --- sanic/app.py | 13 ++++++++++++- sanic/config.py | 5 +++-- sanic/server/loop.py | 9 ++++++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index cbd3ed2fd3..002193535a 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -66,6 +66,7 @@ URLBuildError, ) from sanic.handlers import ErrorHandler +from sanic.helpers import _default from sanic.log import LOGGING_CONFIG_DEFAULTS, Colors, error_logger, logger from sanic.mixins.listeners import ListenerEvent from sanic.models.futures import ( @@ -1102,7 +1103,10 @@ def run( register_sys_signals=register_sys_signals, ) - if self.config.USE_UVLOOP: + if ( + self.config.USE_UVLOOP is True + or (self.config.USE_UVLOOP is _default and not OS_IS_WINDOWS) + ): try_use_uvloop() try: @@ -1222,6 +1226,13 @@ async def create_server( run_async=return_asyncio_server, ) + if self.config.USE_UVLOOP is True: + error_logger.warning( + "You are trying to use uvloop, but this is only supported " + "when using the run(...) method. Sanic will now continue " + "to run using the default event loop." + ) + main_start = server_settings.pop("main_start", None) main_stop = server_settings.pop("main_stop", None) if main_start or main_stop: diff --git a/sanic/config.py b/sanic/config.py index 3b6cf8f859..2cd624c1f7 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -7,6 +7,7 @@ from warnings import warn from sanic.errorpages import check_error_format +from sanic.helpers import Default, _default from sanic.http import Http from sanic.utils import load_module_from_file_location, str_to_bool @@ -40,7 +41,7 @@ "REQUEST_MAX_SIZE": 100000000, # 100 megabytes "REQUEST_TIMEOUT": 60, # 60 seconds "RESPONSE_TIMEOUT": 60, # 60 seconds - "USE_UVLOOP": True, + "USE_UVLOOP": _default, "WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte "WEBSOCKET_PING_INTERVAL": 20, "WEBSOCKET_PING_TIMEOUT": 20, @@ -70,7 +71,7 @@ class Config(dict): REQUEST_TIMEOUT: int RESPONSE_TIMEOUT: int SERVER_NAME: str - USE_UVLOOP: bool + USE_UVLOOP: Union[Default, bool] WEBSOCKET_MAX_SIZE: int WEBSOCKET_PING_INTERVAL: int WEBSOCKET_PING_TIMEOUT: int diff --git a/sanic/server/loop.py b/sanic/server/loop.py index b6283727b9..49d7eb66c3 100644 --- a/sanic/server/loop.py +++ b/sanic/server/loop.py @@ -11,7 +11,14 @@ def try_use_uvloop() -> None: """ Use uvloop instead of the default asyncio loop. """ - if OS_IS_WINDOWS: # uvloop is not compatible + if OS_IS_WINDOWS: + error_logger.warning( + "You are trying to use uvloop, but uvloop is not compatible " + "with your system. You can disable uvloop completely by setting " + "the 'USE_UVLOOP' configuration value to false, or simply not " + "defining it and letting Sanic handle it for you. Sanic will now " + "continue to run using the default event loop." + ) return try: From f4acfd1641a3feaf027eef8706dcafaa49cd57a4 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Tue, 7 Dec 2021 20:18:17 +0100 Subject: [PATCH 38/56] Fix linting --- sanic/app.py | 5 ++--- sanic/server/loop.py | 4 +--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 002193535a..2a7cf7bc07 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1103,9 +1103,8 @@ def run( register_sys_signals=register_sys_signals, ) - if ( - self.config.USE_UVLOOP is True - or (self.config.USE_UVLOOP is _default and not OS_IS_WINDOWS) + if self.config.USE_UVLOOP is True or ( + self.config.USE_UVLOOP is _default and not OS_IS_WINDOWS ): try_use_uvloop() diff --git a/sanic/server/loop.py b/sanic/server/loop.py index 49d7eb66c3..cd6bc4f23e 100644 --- a/sanic/server/loop.py +++ b/sanic/server/loop.py @@ -45,7 +45,5 @@ def try_use_uvloop() -> None: "false." ) - if not isinstance( - asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy - ): + if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy): asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) From 710896b6737ee68064991ddef4f8f54f2d37b0af Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Tue, 7 Dec 2021 20:46:54 +0100 Subject: [PATCH 39/56] Fix tests --- tests/test_app.py | 53 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index ba244501e6..de0192e25b 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -12,7 +12,7 @@ import sanic from sanic import Sanic -from sanic.compat import UVLOOP_INSTALLED +from sanic.compat import OS_IS_WINDOWS, UVLOOP_INSTALLED from sanic.config import Config from sanic.exceptions import SanicException from sanic.response import text @@ -441,48 +441,77 @@ class CustomContext: assert app.ctx == ctx -def test_uvloop_usage(app, monkeypatch): +def test_uvloop_config(app, monkeypatch): @app.get("/test") def handler(request): return text("ok") - use_uvloop = Mock() - monkeypatch.setattr(sanic.app, "use_uvloop", use_uvloop) + try_use_uvloop = Mock() + monkeypatch.setattr(sanic.app, "try_use_uvloop", try_use_uvloop) + # Default config + app.test_client.get("/test") + if OS_IS_WINDOWS: + try_use_uvloop.assert_not_called() + else: + try_use_uvloop.assert_called_once() + + try_use_uvloop.reset_mock() app.config["USE_UVLOOP"] = False app.test_client.get("/test") - use_uvloop.assert_not_called() + try_use_uvloop.assert_not_called() + try_use_uvloop.reset_mock() app.config["USE_UVLOOP"] = True app.test_client.get("/test") - use_uvloop.assert_called_once() + try_use_uvloop.assert_called_once() -def test_uvloop_is_ignored_with_create_server(app, monkeypatch): +def test_uvloop_is_never_called_with_create_server(app, caplog, monkeypatch): @app.get("/test") def handler(request): return text("ok") - use_uvloop = Mock() - monkeypatch.setattr(sanic.app, "use_uvloop", use_uvloop) + try_use_uvloop = Mock() + monkeypatch.setattr(sanic.app, "try_use_uvloop", try_use_uvloop) loop = asyncio.get_event_loop() + # Default config + asyncio_srv_coro = app.create_server( + return_asyncio_server=True, + asyncio_server_kwargs=dict(start_serving=False) + ) + loop.run_until_complete(asyncio_srv_coro) + try_use_uvloop.assert_not_called() + app.config["USE_UVLOOP"] = False asyncio_srv_coro = app.create_server( return_asyncio_server=True, asyncio_server_kwargs=dict(start_serving=False) ) loop.run_until_complete(asyncio_srv_coro) - use_uvloop.assert_not_called() + try_use_uvloop.assert_not_called() app.config["USE_UVLOOP"] = True asyncio_srv_coro = app.create_server( return_asyncio_server=True, asyncio_server_kwargs=dict(start_serving=False) ) - loop.run_until_complete(asyncio_srv_coro) - use_uvloop.assert_not_called() + + with caplog.at_level(logging.WARNING): + loop.run_until_complete(asyncio_srv_coro) + try_use_uvloop.assert_not_called() + + for record in caplog.records: + if record.message.startswith("You are trying to use"): + break + + assert record.message == ( + "You are trying to use uvloop, but this is only supported " + "when using the run(...) method. Sanic will now continue " + "to run using the default event loop." + ) def test_cannot_run_fast_and_workers(app): From 72152ce5da46ca63b181a2a6fbdeee09fe77bdf7 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Tue, 7 Dec 2021 21:09:16 +0100 Subject: [PATCH 40/56] Added some tests --- tests/test_server_loop.py | 49 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/test_server_loop.py diff --git a/tests/test_server_loop.py b/tests/test_server_loop.py new file mode 100644 index 0000000000..e53672c0e8 --- /dev/null +++ b/tests/test_server_loop.py @@ -0,0 +1,49 @@ +import logging +import pytest + +from sanic.server import loop +from sanic.compat import OS_IS_WINDOWS, UVLOOP_INSTALLED + + +@pytest.mark.skipif( + not OS_IS_WINDOWS, + reason="Not testable with current client", +) +def test_raises_warning_if_os_is_windows(caplog): + with caplog.at_level(logging.WARNING): + loop.try_use_uvloop() + + for record in caplog.records: + if record.message.startswith("You are trying to use"): + break + + assert record.message == ( + "You are trying to use uvloop, but uvloop is not compatible " + "with your system. You can disable uvloop completely by setting " + "the 'USE_UVLOOP' configuration value to false, or simply not " + "defining it and letting Sanic handle it for you. Sanic will now " + "continue to run using the default event loop." + ) + + +@pytest.mark.skipif( + OS_IS_WINDOWS, + reason="Not testable with current client", +) +def test_raises_warning_if_uvloop_not_installed(caplog): + if not UVLOOP_INSTALLED: + with caplog.at_level(logging.WARNING): + loop.try_use_uvloop() + + for record in caplog.records: + if record.message.startswith("You are trying to use"): + break + + assert record.message == ( + "You are trying to use uvloop, but uvloop is not " + "installed in your system. In order to use uvloop " + "you must first install it. Otherwise, you can disable " + "uvloop completely by setting the 'USE_UVLOOP' " + "configuration value to false. Sanic will now continue " + "to run with the default event loop." + ) From a3906fc5b8892f8d1c4af381e6019af8f76d0196 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Wed, 8 Dec 2021 16:53:51 +0100 Subject: [PATCH 41/56] Use `getenv` for easier mocking --- sanic/server/loop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/server/loop.py b/sanic/server/loop.py index cd6bc4f23e..5613f709f3 100644 --- a/sanic/server/loop.py +++ b/sanic/server/loop.py @@ -1,7 +1,7 @@ import asyncio -import os from distutils.util import strtobool +from os import getenv from sanic.compat import OS_IS_WINDOWS from sanic.log import error_logger @@ -34,7 +34,7 @@ def try_use_uvloop() -> None: ) return - uvloop_install_removed = strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")) + uvloop_install_removed = strtobool(getenv("SANIC_NO_UVLOOP", "no")) if uvloop_install_removed: error_logger.info( "You are requesting to run Sanic using uvloop, but the " From 3c58c09f2c6bf2da910f682530d6ba7590b5ca08 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Wed, 8 Dec 2021 17:53:26 +0100 Subject: [PATCH 42/56] Added missing tests --- tests/test_server_loop.py | 96 ++++++++++++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 17 deletions(-) diff --git a/tests/test_server_loop.py b/tests/test_server_loop.py index e53672c0e8..1e66a761c6 100644 --- a/tests/test_server_loop.py +++ b/tests/test_server_loop.py @@ -1,4 +1,7 @@ import logging + +from unittest.mock import Mock, patch + import pytest from sanic.server import loop @@ -27,23 +30,82 @@ def test_raises_warning_if_os_is_windows(caplog): @pytest.mark.skipif( - OS_IS_WINDOWS, + OS_IS_WINDOWS or UVLOOP_INSTALLED, reason="Not testable with current client", ) def test_raises_warning_if_uvloop_not_installed(caplog): - if not UVLOOP_INSTALLED: - with caplog.at_level(logging.WARNING): - loop.try_use_uvloop() - - for record in caplog.records: - if record.message.startswith("You are trying to use"): - break - - assert record.message == ( - "You are trying to use uvloop, but uvloop is not " - "installed in your system. In order to use uvloop " - "you must first install it. Otherwise, you can disable " - "uvloop completely by setting the 'USE_UVLOOP' " - "configuration value to false. Sanic will now continue " - "to run with the default event loop." - ) + with caplog.at_level(logging.WARNING): + loop.try_use_uvloop() + + for record in caplog.records: + if record.message.startswith("You are trying to use"): + break + + assert record.message == ( + "You are trying to use uvloop, but uvloop is not " + "installed in your system. In order to use uvloop " + "you must first install it. Otherwise, you can disable " + "uvloop completely by setting the 'USE_UVLOOP' " + "configuration value to false. Sanic will now continue " + "to run with the default event loop." + ) + + +@pytest.mark.skipif( + OS_IS_WINDOWS or not UVLOOP_INSTALLED, + reason="Not testable with current client", +) +def test_logs_when_install_and_runtime_config_mismatch(caplog, monkeypatch): + getenv = Mock(return_value="no") + monkeypatch.setattr(loop, "getenv", getenv) + + with caplog.at_level(logging.INFO): + loop.try_use_uvloop() + + assert getenv.assert_called_once_with("SANIC_NO_UVLOOP", "no") + assert caplog.record_tuples == [] + + getenv.reset_mock(return_value="yes") + with caplog.at_level(logging.INFO): + loop.try_use_uvloop() + + assert getenv.assert_called_once_with("SANIC_NO_UVLOOP", "no") + for record in caplog.records: + if record.message.startswith("You are requesting to run"): + break + + assert record.message == ( + "You are requesting to run Sanic using uvloop, but the " + "install-time 'SANIC_NO_UVLOOP' environment variable (used to " + "opt-out of installing uvloop with Sanic) is set to true. If " + "you want to prevent Sanic from overriding the event loop policy " + "during runtime, set the 'USE_UVLOOP' configuration value to " + "false." + ) + + +@pytest.mark.skipif( + OS_IS_WINDOWS or not UVLOOP_INSTALLED, + reason="Not testable with current client", +) +def test_sets_loop_policy_only_when_not_already_set(monkeypatch): + import uvloop # type: ignore + + get_event_loop_policy = Mock(return_value=None) + monkeypatch.setattr( + loop.asyncio, "get_event_loop_policy", get_event_loop_policy + ) + + with patch("asyncio.set_event_loop_policy") as set_event_loop_policy: + # Existing policy is not uvloop.EventLoopPolicy + loop.try_use_uvloop() + set_event_loop_policy.assert_called_once() + policy = set_event_loop_policy.call_args[0] + assert isinstance(policy, uvloop.EventLoopPolicy) + + get_event_loop_policy.reset_mock(return_value=policy) + set_event_loop_policy.reset_mock() + + # Existing policy is uvloop.EventLoopPolicy + loop.try_use_uvloop() + set_event_loop_policy.assert_not_called() From d4d6c61c4f2f1195d4f8cc241329c639a3b40552 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Wed, 8 Dec 2021 18:03:21 +0100 Subject: [PATCH 43/56] Fix assertions --- tests/test_server_loop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_server_loop.py b/tests/test_server_loop.py index 1e66a761c6..5e11032810 100644 --- a/tests/test_server_loop.py +++ b/tests/test_server_loop.py @@ -62,14 +62,14 @@ def test_logs_when_install_and_runtime_config_mismatch(caplog, monkeypatch): with caplog.at_level(logging.INFO): loop.try_use_uvloop() - assert getenv.assert_called_once_with("SANIC_NO_UVLOOP", "no") + getenv.assert_called_once_with("SANIC_NO_UVLOOP", "no") assert caplog.record_tuples == [] getenv.reset_mock(return_value="yes") with caplog.at_level(logging.INFO): loop.try_use_uvloop() - assert getenv.assert_called_once_with("SANIC_NO_UVLOOP", "no") + getenv.assert_called_once_with("SANIC_NO_UVLOOP", "no") for record in caplog.records: if record.message.startswith("You are requesting to run"): break From 08e1ce5e0d909c1f40c8ae7c9be7a2396d00d9eb Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Wed, 8 Dec 2021 18:35:06 +0100 Subject: [PATCH 44/56] Fix tests --- tests/test_server_loop.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_server_loop.py b/tests/test_server_loop.py index 5e11032810..617bf147d7 100644 --- a/tests/test_server_loop.py +++ b/tests/test_server_loop.py @@ -65,7 +65,8 @@ def test_logs_when_install_and_runtime_config_mismatch(caplog, monkeypatch): getenv.assert_called_once_with("SANIC_NO_UVLOOP", "no") assert caplog.record_tuples == [] - getenv.reset_mock(return_value="yes") + getenv = Mock(return_value="yes") + monkeypatch.setattr(loop, "getenv", getenv) with caplog.at_level(logging.INFO): loop.try_use_uvloop() @@ -100,7 +101,7 @@ def test_sets_loop_policy_only_when_not_already_set(monkeypatch): # Existing policy is not uvloop.EventLoopPolicy loop.try_use_uvloop() set_event_loop_policy.assert_called_once() - policy = set_event_loop_policy.call_args[0] + policy = set_event_loop_policy.call_args.args[0] assert isinstance(policy, uvloop.EventLoopPolicy) get_event_loop_policy.reset_mock(return_value=policy) From d694525a336e062f77144828e950e376599f8eca Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Wed, 8 Dec 2021 18:56:10 +0100 Subject: [PATCH 45/56] Fix tests for real this time --- tests/test_server_loop.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_server_loop.py b/tests/test_server_loop.py index 617bf147d7..164404f248 100644 --- a/tests/test_server_loop.py +++ b/tests/test_server_loop.py @@ -92,21 +92,24 @@ def test_logs_when_install_and_runtime_config_mismatch(caplog, monkeypatch): def test_sets_loop_policy_only_when_not_already_set(monkeypatch): import uvloop # type: ignore + # Existing policy is not uvloop.EventLoopPolicy get_event_loop_policy = Mock(return_value=None) monkeypatch.setattr( loop.asyncio, "get_event_loop_policy", get_event_loop_policy ) with patch("asyncio.set_event_loop_policy") as set_event_loop_policy: - # Existing policy is not uvloop.EventLoopPolicy loop.try_use_uvloop() set_event_loop_policy.assert_called_once() policy = set_event_loop_policy.call_args.args[0] assert isinstance(policy, uvloop.EventLoopPolicy) - get_event_loop_policy.reset_mock(return_value=policy) - set_event_loop_policy.reset_mock() + # Existing policy is uvloop.EventLoopPolicy + get_event_loop_policy = Mock(return_value=policy) + monkeypatch.setattr( + loop.asyncio, "get_event_loop_policy", get_event_loop_policy + ) - # Existing policy is uvloop.EventLoopPolicy + with patch("asyncio.set_event_loop_policy") as set_event_loop_policy: loop.try_use_uvloop() set_event_loop_policy.assert_not_called() From a7d52072df9e4fe96533b84ff4e8edd7c274a642 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Wed, 8 Dec 2021 19:14:23 +0100 Subject: [PATCH 46/56] Fix tests for python 3.7 --- tests/test_server_loop.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_server_loop.py b/tests/test_server_loop.py index 164404f248..fbd5cc2bb3 100644 --- a/tests/test_server_loop.py +++ b/tests/test_server_loop.py @@ -101,7 +101,8 @@ def test_sets_loop_policy_only_when_not_already_set(monkeypatch): with patch("asyncio.set_event_loop_policy") as set_event_loop_policy: loop.try_use_uvloop() set_event_loop_policy.assert_called_once() - policy = set_event_loop_policy.call_args.args[0] + args, _ = set_event_loop_policy.call_args + policy = args[0] assert isinstance(policy, uvloop.EventLoopPolicy) # Existing policy is uvloop.EventLoopPolicy From 868a2da1bb84964f772ec159c8534de52f79a1a2 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Wed, 8 Dec 2021 19:19:51 +0100 Subject: [PATCH 47/56] Add warning when configuring loop with `create_server` --- sanic/app.py | 8 ++++---- tests/test_app.py | 23 ++++++++++++++++++----- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 00f0ddc7c2..847fe1803a 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1223,11 +1223,11 @@ async def create_server( run_async=return_asyncio_server, ) - if self.config.USE_UVLOOP is True: + if self.config.USE_UVLOOP is not _default: error_logger.warning( - "You are trying to use uvloop, but this is only supported " - "when using the run(...) method. Sanic will now continue " - "to run using the default event loop." + "You are trying to configure uvloop, but this is only " + "supported when using the run(...) method. Sanic will now " + "continue to run using the existing event loop." ) main_start = server_settings.pop("main_start", None) diff --git a/tests/test_app.py b/tests/test_app.py index de0192e25b..e99967b258 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -490,7 +490,20 @@ def handler(request): return_asyncio_server=True, asyncio_server_kwargs=dict(start_serving=False) ) - loop.run_until_complete(asyncio_srv_coro) + + with caplog.at_level(logging.WARNING): + loop.run_until_complete(asyncio_srv_coro) + + for record in caplog.records: + if record.message.startswith("You are trying to configure"): + break + + assert record.message == ( + "You are trying to configure uvloop, but this is only " + "supported when using the run(...) method. Sanic will now " + "continue to run using the existing event loop." + ) + try_use_uvloop.assert_not_called() app.config["USE_UVLOOP"] = True @@ -504,13 +517,13 @@ def handler(request): try_use_uvloop.assert_not_called() for record in caplog.records: - if record.message.startswith("You are trying to use"): + if record.message.startswith("You are trying to configure"): break assert record.message == ( - "You are trying to use uvloop, but this is only supported " - "when using the run(...) method. Sanic will now continue " - "to run using the default event loop." + "You are trying to configure uvloop, but this is only " + "supported when using the run(...) method. Sanic will now " + "continue to run using the existing event loop." ) From 19069d66301953bd02220e8ac9c909373ba99050 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sat, 18 Dec 2021 01:04:05 +0100 Subject: [PATCH 48/56] Ensure consistent uvloop config across apps --- sanic/app.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/sanic/app.py b/sanic/app.py index 426ffe8e2a..d81c659e15 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -128,6 +128,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): "_state", "_test_client", "_test_manager", + "_uvloop_setting", # TODO: Remove in vXX.X "asgi", "auto_reload", "auto_reload", @@ -157,6 +158,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): ) _app_registry: Dict[str, "Sanic"] = {} + _uvloop_setting = _default test_mode = False def __init__( @@ -223,6 +225,12 @@ def __init__( self.go_fast = self.run if register is not None: + warn( + "The register argument is deprecated and will stop working " + "in vXX.X. After vXX.X all apps will added to the Sanic app " + "registry.", + DeprecationWarning, + ) self.config.REGISTER = register if self.config.REGISTER: self.__class__.register_app(self) @@ -1713,6 +1721,19 @@ async def _startup(self): self._future_registry.clear() self.signalize() self.finalize() + + # TODO: Replace in vXX.X to check with apps in app registry + if ( + self._uvloop_setting is not _default + and self._uvloop_setting != self.config.USE_UVLOOP + ): + error_logger.warn( + "It looks like you're running several apps with different " + "uvloop settings. This is not supported and may lead to " + "unintended behaviour." + ) + self._uvloop_setting = self.config.USE_UVLOOP + ErrorHandler.finalize( self.error_handler, fallback=self.config.FALLBACK_ERROR_FORMAT ) From 2a85824870296b9d1adec0912e8eb31e45fd0143 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sat, 18 Dec 2021 01:04:20 +0100 Subject: [PATCH 49/56] Add warning when changing loop in ASGI mode --- sanic/asgi.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sanic/asgi.py b/sanic/asgi.py index 00b181dcde..22a1a1cc04 100644 --- a/sanic/asgi.py +++ b/sanic/asgi.py @@ -13,6 +13,7 @@ from sanic.response import BaseHTTPResponse from sanic.server import ConnInfo from sanic.server.websockets.connection import WebSocketConnection +from sanic.helpers import _default class Lifespan: @@ -53,6 +54,13 @@ async def startup(self) -> None: await self.asgi_app.sanic_app._server_event("init", "before") await self.asgi_app.sanic_app._server_event("init", "after") + if self.asgi_app.sanic_app.config.USE_UVLOOP is not _default: + warnings.warn( + "You have set the USE_UVLOOP configuration option, but Sanic " + "cannot control the event loop when running in ASGI mode." + "This option will be ignored." + ) + async def shutdown(self) -> None: """ Gather the listeners to fire on server stop. From e608ae7f64f8c144e3fd8df1b0895a5a235456f4 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sat, 18 Dec 2021 01:07:50 +0100 Subject: [PATCH 50/56] Reorder imports --- sanic/asgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/asgi.py b/sanic/asgi.py index 22a1a1cc04..5ef15a916e 100644 --- a/sanic/asgi.py +++ b/sanic/asgi.py @@ -7,13 +7,13 @@ from sanic.compat import Header from sanic.exceptions import ServerError +from sanic.helpers import _default from sanic.http import Stage from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport from sanic.request import Request from sanic.response import BaseHTTPResponse from sanic.server import ConnInfo from sanic.server.websockets.connection import WebSocketConnection -from sanic.helpers import _default class Lifespan: From aa1639ed01e3a2f9d839a1e56925fc25199d59a5 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sat, 18 Dec 2021 01:40:56 +0100 Subject: [PATCH 51/56] Reword message --- sanic/app.py | 7 ++++--- tests/test_app.py | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index d81c659e15..c798e4ba54 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1291,9 +1291,10 @@ async def create_server( if self.config.USE_UVLOOP is not _default: error_logger.warning( - "You are trying to configure uvloop, but this is only " - "supported when using the run(...) method. Sanic will now " - "continue to run using the existing event loop." + "You are trying to change the uvloop configuration, but " + "this is only effective when using the run(...) method. " + "When using the create_server(...) method Sanic will use " + "the already existing loop." ) main_start = server_settings.pop("main_start", None) diff --git a/tests/test_app.py b/tests/test_app.py index b4d18282c5..e256f38c3c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -545,9 +545,10 @@ def handler(request): break assert record.message == ( - "You are trying to configure uvloop, but this is only " - "supported when using the run(...) method. Sanic will now " - "continue to run using the existing event loop." + "You are trying to change the uvloop configuration, but " + "this is only effective when using the run(...) method. " + "When using the create_server(...) method Sanic will use " + "the already existing loop." ) From fbe0b718308f15afc74f57d24e64d7c5d8271ef6 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sat, 18 Dec 2021 01:55:43 +0100 Subject: [PATCH 52/56] Fix the other half of the test --- tests/test_app.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index e256f38c3c..b677d1f2dd 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -519,13 +519,14 @@ def handler(request): loop.run_until_complete(asyncio_srv_coro) for record in caplog.records: - if record.message.startswith("You are trying to configure"): + if record.message.startswith("You are trying to change"): break assert record.message == ( - "You are trying to configure uvloop, but this is only " - "supported when using the run(...) method. Sanic will now " - "continue to run using the existing event loop." + "You are trying to change the uvloop configuration, but " + "this is only effective when using the run(...) method. " + "When using the create_server(...) method Sanic will use " + "the already existing loop." ) try_use_uvloop.assert_not_called() @@ -541,7 +542,7 @@ def handler(request): try_use_uvloop.assert_not_called() for record in caplog.records: - if record.message.startswith("You are trying to configure"): + if record.message.startswith("You are trying to change"): break assert record.message == ( From 51c7278966deca84f0e55c4cfc732f761c0743c0 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Sat, 18 Dec 2021 21:09:10 +0100 Subject: [PATCH 53/56] Added app logging tests and fixed classvar not being set --- sanic/app.py | 14 +++--- tests/test_app.py | 106 +++++++++++++++++++++++++++++----------------- 2 files changed, 73 insertions(+), 47 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index c798e4ba54..b82f16a928 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -158,7 +158,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): ) _app_registry: Dict[str, "Sanic"] = {} - _uvloop_setting = _default + _uvloop_setting = None test_mode = False def __init__( @@ -227,8 +227,8 @@ def __init__( if register is not None: warn( "The register argument is deprecated and will stop working " - "in vXX.X. After vXX.X all apps will added to the Sanic app " - "registry.", + "in vXX.X. After vXX.X all apps will be added to the Sanic " + "app registry.", DeprecationWarning, ) self.config.REGISTER = register @@ -1725,15 +1725,15 @@ async def _startup(self): # TODO: Replace in vXX.X to check with apps in app registry if ( - self._uvloop_setting is not _default - and self._uvloop_setting != self.config.USE_UVLOOP + self.__class__._uvloop_setting is not None + and self.__class__._uvloop_setting != self.config.USE_UVLOOP ): - error_logger.warn( + error_logger.warning( "It looks like you're running several apps with different " "uvloop settings. This is not supported and may lead to " "unintended behaviour." ) - self._uvloop_setting = self.config.USE_UVLOOP + self.__class__._uvloop_setting = self.config.USE_UVLOOP ErrorHandler.finalize( self.error_handler, fallback=self.config.FALLBACK_ERROR_FORMAT diff --git a/tests/test_app.py b/tests/test_app.py index b677d1f2dd..6e5dff2606 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -2,6 +2,7 @@ import logging import re +from collections import Counter from inspect import isawaitable from os import environ from unittest.mock import Mock, patch @@ -14,6 +15,7 @@ from sanic.config import Config from sanic.exceptions import SanicException from sanic.response import text +from sanic.helpers import _default @pytest.fixture(autouse=True) @@ -390,6 +392,22 @@ def test_app_no_registry(): Sanic.get_app("no-register") +def test_app_no_registry_deprecation_message(): + with pytest.warns(DeprecationWarning) as records: + Sanic("no-register", register=False) + Sanic("no-register", register=True) + + message = ( + "The register argument is deprecated and will stop working " + "in vXX.X. After vXX.X all apps will be added to the Sanic " + "app registry." + ) + + assert len(records) == 2 + for record in records: + assert record.message.args[0] == message + + def test_app_no_registry_env(): environ["SANIC_REGISTER"] = "False" Sanic("no-register") @@ -491,67 +509,75 @@ def handler(request): try_use_uvloop.assert_called_once() -def test_uvloop_is_never_called_with_create_server(app, caplog, monkeypatch): - @app.get("/test") - def handler(request): - return text("ok") +def test_uvloop_cannot_never_called_with_create_server(caplog, monkeypatch): + apps = ( + Sanic("default-uvloop"), + Sanic("no-uvloop"), + Sanic("yes-uvloop") + ) + + apps[1].config.USE_UVLOOP = False + apps[2].config.USE_UVLOOP = True try_use_uvloop = Mock() monkeypatch.setattr(sanic.app, "try_use_uvloop", try_use_uvloop) loop = asyncio.get_event_loop() - # Default config - asyncio_srv_coro = app.create_server( - return_asyncio_server=True, - asyncio_server_kwargs=dict(start_serving=False) - ) - loop.run_until_complete(asyncio_srv_coro) - try_use_uvloop.assert_not_called() - - app.config["USE_UVLOOP"] = False - asyncio_srv_coro = app.create_server( - return_asyncio_server=True, - asyncio_server_kwargs=dict(start_serving=False) - ) - with caplog.at_level(logging.WARNING): - loop.run_until_complete(asyncio_srv_coro) + for app in apps: + srv_coro = app.create_server( + return_asyncio_server=True, + asyncio_server_kwargs=dict(start_serving=False) + ) + loop.run_until_complete(srv_coro) - for record in caplog.records: - if record.message.startswith("You are trying to change"): - break + try_use_uvloop.assert_not_called() # Check it didn't try to change policy - assert record.message == ( + message = ( "You are trying to change the uvloop configuration, but " "this is only effective when using the run(...) method. " "When using the create_server(...) method Sanic will use " "the already existing loop." ) - try_use_uvloop.assert_not_called() + counter = Counter([(r[1], r[2]) for r in caplog.record_tuples]) + modified = sum(1 for app in apps if app.config.USE_UVLOOP is not _default) - app.config["USE_UVLOOP"] = True - asyncio_srv_coro = app.create_server( - return_asyncio_server=True, - asyncio_server_kwargs=dict(start_serving=False) - ) + assert counter[(logging.WARNING, message)] == modified - with caplog.at_level(logging.WARNING): - loop.run_until_complete(asyncio_srv_coro) - try_use_uvloop.assert_not_called() - for record in caplog.records: - if record.message.startswith("You are trying to change"): - break +def test_multiple_uvloop_configs_display_warning(caplog): + Sanic._uvloop_setting = None # Reset the setting (changed in prev tests) - assert record.message == ( - "You are trying to change the uvloop configuration, but " - "this is only effective when using the run(...) method. " - "When using the create_server(...) method Sanic will use " - "the already existing loop." + default_uvloop = Sanic("default-uvloop") + no_uvloop = Sanic("no-uvloop") + yes_uvloop = Sanic("yes-uvloop") + + no_uvloop.config.USE_UVLOOP = False + yes_uvloop.config.USE_UVLOOP = True + + loop = asyncio.get_event_loop() + + with caplog.at_level(logging.WARNING): + for app in (default_uvloop, no_uvloop, yes_uvloop): + srv_coro = app.create_server( + return_asyncio_server=True, + asyncio_server_kwargs=dict(start_serving=False) + ) + srv = loop.run_until_complete(srv_coro) + loop.run_until_complete(srv.startup()) + + message = ( + "It looks like you're running several apps with different " + "uvloop settings. This is not supported and may lead to " + "unintended behaviour." ) + counter = Counter([(r[1], r[2]) for r in caplog.record_tuples]) + + assert counter[(logging.WARNING, message)] == 2 + def test_cannot_run_fast_and_workers(app): message = "You cannot use both fast=True and workers=X" From 578f35401319efb2a7a155f46c939877ca9c9512 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Sun, 19 Dec 2021 09:41:52 +0200 Subject: [PATCH 54/56] Add test for ASGI --- tests/test_asgi.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_asgi.py b/tests/test_asgi.py index 3d464a4f55..d00a70bdb8 100644 --- a/tests/test_asgi.py +++ b/tests/test_asgi.py @@ -145,6 +145,37 @@ def install_signal_handlers(self): assert after_server_stop +def test_non_default_uvloop_config_raises_warning(app): + app.config.USE_UVLOOP = True + + class CustomServer(uvicorn.Server): + def install_signal_handlers(self): + pass + + config = uvicorn.Config(app=app, loop="asyncio", limit_max_requests=0) + server = CustomServer(config=config) + + with pytest.warns(UserWarning) as records: + server.run() + + all_tasks = asyncio.all_tasks(asyncio.get_event_loop()) + for task in all_tasks: + task.cancel() + + msg = "" + for record in records: + _msg = str(record.message) + if _msg.startswith("You have set the USE_UVLOOP configuration"): + msg = _msg + break + + assert msg == ( + "You have set the USE_UVLOOP configuration option, but Sanic " + "cannot control the event loop when running in ASGI mode." + "This option will be ignored." + ) + + @pytest.mark.asyncio async def test_mockprotocol_events(protocol): assert protocol._not_paused.is_set() From 373ab7f203055c83572bec7fa613605c210ca579 Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Mon, 20 Dec 2021 19:59:26 +0100 Subject: [PATCH 55/56] Remove registry deprecation --- sanic/app.py | 6 ------ tests/test_app.py | 16 ---------------- 2 files changed, 22 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 08837e7d36..7c2d232047 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -223,12 +223,6 @@ def __init__( self.go_fast = self.run if register is not None: - warn( - "The register argument is deprecated and will stop working " - "in vXX.X. After vXX.X all apps will be added to the Sanic " - "app registry.", - DeprecationWarning, - ) self.config.REGISTER = register if self.config.REGISTER: self.__class__.register_app(self) diff --git a/tests/test_app.py b/tests/test_app.py index 6e5dff2606..6ce8ae65c0 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -392,22 +392,6 @@ def test_app_no_registry(): Sanic.get_app("no-register") -def test_app_no_registry_deprecation_message(): - with pytest.warns(DeprecationWarning) as records: - Sanic("no-register", register=False) - Sanic("no-register", register=True) - - message = ( - "The register argument is deprecated and will stop working " - "in vXX.X. After vXX.X all apps will be added to the Sanic " - "app registry." - ) - - assert len(records) == 2 - for record in records: - assert record.message.args[0] == message - - def test_app_no_registry_env(): environ["SANIC_REGISTER"] = "False" Sanic("no-register") From b290d0ffe0b9e28b5e3aba34210155a9181c14ff Mon Sep 17 00:00:00 2001 From: prryplatypus <25409753+prryplatypus@users.noreply.github.com> Date: Mon, 20 Dec 2021 20:00:36 +0100 Subject: [PATCH 56/56] Update TODO version in comments --- sanic/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 7c2d232047..2cd3b4996d 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -128,7 +128,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): "_state", "_test_client", "_test_manager", - "_uvloop_setting", # TODO: Remove in vXX.X + "_uvloop_setting", # TODO: Remove in v22.6 "asgi", "auto_reload", "auto_reload", @@ -1715,7 +1715,7 @@ async def _startup(self): self.signalize() self.finalize() - # TODO: Replace in vXX.X to check with apps in app registry + # TODO: Replace in v22.6 to check against apps in app registry if ( self.__class__._uvloop_setting is not None and self.__class__._uvloop_setting != self.config.USE_UVLOOP