From c237687ca63f488025c96c2a16b7d49cb39fb70c Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Sat, 8 Jan 2022 22:57:41 +0200 Subject: [PATCH 1/5] Deprecations --- sanic/app.py | 9 +++-- sanic/cli/app.py | 12 ++---- sanic/cli/arguments.py | 15 ++++---- sanic/handlers.py | 40 +++----------------- sanic/server/protocols/websocket_protocol.py | 24 ------------ tests/test_cli.py | 17 ++++++++- tests/test_exceptions_handler.py | 27 +------------ 7 files changed, 38 insertions(+), 106 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 97c7a177b2..56d2b83580 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1061,6 +1061,7 @@ def run( host: Optional[str] = None, port: Optional[int] = None, *, + dev: bool = False, debug: bool = False, auto_reload: Optional[bool] = None, ssl: Union[None, SSLContext, dict, str, list, tuple] = None, @@ -1143,10 +1144,12 @@ def run( "#asynchronous-support" ) - if auto_reload or auto_reload is None and debug: + if dev: + debug = True auto_reload = True - if os.environ.get("SANIC_SERVER_RUNNING") != "true": - return reloader_helpers.watchdog(1.0, self) + + if auto_reload and os.environ.get("SANIC_SERVER_RUNNING") != "true": + return reloader_helpers.watchdog(1.0, self) if sock is None: host, port = host or "127.0.0.1", port or 8000 diff --git a/sanic/cli/app.py b/sanic/cli/app.py index 3001b6e1fa..460e5c872b 100644 --- a/sanic/cli/app.py +++ b/sanic/cli/app.py @@ -79,13 +79,6 @@ def run(self): error_logger.exception("Failed to run app") def _precheck(self): - if self.args.debug and self.main_process: - error_logger.warning( - "Starting in v22.3, --debug will no " - "longer automatically run the auto-reloader.\n Switch to " - "--dev to continue using that functionality." - ) - # # Custom TLS mismatch handling for better diagnostics if self.main_process and ( # one of cert/key missing @@ -174,8 +167,9 @@ def _build_run_kwargs(self): "workers": self.args.workers, } - if self.args.auto_reload: - kwargs["auto_reload"] = True + for maybe_arg in ("auto_reload", "dev"): + if getattr(self.args, maybe_arg, False): + kwargs[maybe_arg] = True if self.args.path: if self.args.auto_reload or self.args.debug: diff --git a/sanic/cli/arguments.py b/sanic/cli/arguments.py index 20644bdc45..bd63e362c6 100644 --- a/sanic/cli/arguments.py +++ b/sanic/cli/arguments.py @@ -180,19 +180,18 @@ def attach(self): "--debug", dest="debug", action="store_true", - help="Run the server in debug mode", + help=( + "Run the server in DEBUG mode. It includes DEBUG logging, " + "additional context on exceptions, and other settings " + "not-safe for PRODUCTION, but helpful for debugging problems." + ), ) self.container.add_argument( "-d", "--dev", - dest="debug", + dest="dev", action="store_true", - help=( - "Currently is an alias for --debug. But starting in v22.3, \n" - "--debug will no longer automatically trigger auto_restart. \n" - "However, --dev will continue, effectively making it the \n" - "same as debug + auto_reload." - ), + help=("Debug + auto_reload."), ) self.container.add_argument( "-r", diff --git a/sanic/handlers.py b/sanic/handlers.py index 44ff77a74e..c5859597be 100644 --- a/sanic/handlers.py +++ b/sanic/handlers.py @@ -1,13 +1,12 @@ from __future__ import annotations -from inspect import signature from typing import Dict, List, Optional, Tuple, Type, Union from sanic.config import Config from sanic.errorpages import ( DEFAULT_FORMAT, BaseRenderer, - HTMLRenderer, + TextRenderer, exception_response, ) from sanic.exceptions import ( @@ -35,13 +34,11 @@ class ErrorHandler: """ - # Beginning in v22.3, the base renderer will be TextRenderer def __init__( self, fallback: Union[str, Default] = _default, - base: Type[BaseRenderer] = HTMLRenderer, + base: Type[BaseRenderer] = TextRenderer, ): - self.handlers: List[Tuple[Type[BaseException], RouteHandler]] = [] self.cached_handlers: Dict[ Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler] ] = {} @@ -95,8 +92,8 @@ def _get_fallback_value(cls, error_handler: ErrorHandler, config: Config): def finalize( cls, error_handler: ErrorHandler, + config: Config, fallback: Optional[str] = None, - config: Optional[Config] = None, ): if fallback: deprecation( @@ -107,14 +104,10 @@ def finalize( 22.6, ) - if config is None: - deprecation( - "Starting in v22.3, config will be a required argument " - "for ErrorHandler.finalize().", - 22.3, - ) + if not fallback: + fallback = config.FALLBACK_ERROR_FORMAT - if fallback and fallback != DEFAULT_FORMAT: + if fallback != DEFAULT_FORMAT: if error_handler._fallback is not _default: error_logger.warning( f"Setting the fallback value to {fallback}. This changes " @@ -128,27 +121,9 @@ def finalize( f"Error handler is non-conforming: {type(error_handler)}" ) - sig = signature(error_handler.lookup) - if len(sig.parameters) == 1: - deprecation( - "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.", - 22.3, - ) - legacy_lookup = error_handler._legacy_lookup - error_handler._lookup = legacy_lookup # type: ignore - def _full_lookup(self, exception, route_name: Optional[str] = None): return self.lookup(exception, route_name) - def _legacy_lookup(self, exception, route_name: Optional[str] = None): - return self.lookup(exception) - def add(self, exception, handler, route_names: Optional[List[str]] = None): """ Add a new exception handler to an already existing handler object. @@ -162,9 +137,6 @@ def add(self, exception, handler, route_names: Optional[List[str]] = None): :return: None """ - # self.handlers is deprecated and will be removed in version 22.3 - self.handlers.append((exception, handler)) - if route_names: for route in route_names: self.cached_handlers[(exception, route)] = handler diff --git a/sanic/server/protocols/websocket_protocol.py b/sanic/server/protocols/websocket_protocol.py index ad6f8f952b..aeccd973b4 100644 --- a/sanic/server/protocols/websocket_protocol.py +++ b/sanic/server/protocols/websocket_protocol.py @@ -29,9 +29,6 @@ def __init__( *args, websocket_timeout: float = 10.0, websocket_max_size: Optional[int] = None, - websocket_max_queue: Optional[int] = None, # max_queue is deprecated - websocket_read_limit: Optional[int] = None, # read_limit is deprecated - websocket_write_limit: Optional[int] = None, # write_limit deprecated websocket_ping_interval: Optional[float] = 20.0, websocket_ping_timeout: Optional[float] = 20.0, **kwargs, @@ -40,27 +37,6 @@ def __init__( self.websocket: Optional[WebsocketImplProtocol] = None self.websocket_timeout = websocket_timeout self.websocket_max_size = websocket_max_size - if websocket_max_queue is not None and websocket_max_queue > 0: - # TODO: Reminder remove this warning in v22.3 - deprecation( - "Websocket no longer uses queueing, so websocket_max_queue" - " is no longer required.", - 22.3, - ) - if websocket_read_limit is not None and websocket_read_limit > 0: - # TODO: Reminder remove this warning in v22.3 - deprecation( - "Websocket no longer uses read buffers, so " - "websocket_read_limit is not required.", - 22.3, - ) - if websocket_write_limit is not None and websocket_write_limit > 0: - # TODO: Reminder remove this warning in v22.3 - deprecation( - "Websocket no longer uses write buffers, so " - "websocket_write_limit is not required.", - 22.3, - ) self.websocket_ping_interval = websocket_ping_interval self.websocket_ping_timeout = websocket_ping_timeout diff --git a/tests/test_cli.py b/tests/test_cli.py index 254c91d449..494d37a467 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -103,7 +103,7 @@ def test_tls_wrong_options(cmd): assert not out lines = err.decode().split("\n") - errmsg = lines[8] + errmsg = lines[6] assert errmsg == "TLS certificates must be specified by either of:" @@ -200,13 +200,25 @@ def test_num_workers(num, cmd): assert len(worker_lines) == num * 2, f"Lines found: {lines}" -@pytest.mark.parametrize("cmd", ("--debug", "-d")) +@pytest.mark.parametrize("cmd", ("--debug",)) def test_debug(cmd): command = ["sanic", "fake.server.app", cmd] out, err, exitcode = capture(command) lines = out.split(b"\n") info = read_app_info(lines) + assert info["debug"] is True + assert info["auto_reload"] is False + assert "dev" not in info + + +@pytest.mark.parametrize("cmd", ("--dev", "-d")) +def test_dev(cmd): + command = ["sanic", "fake.server.app", cmd] + out, err, exitcode = capture(command) + lines = out.split(b"\n") + info = read_app_info(lines) + assert info["debug"] is True assert info["auto_reload"] is True @@ -220,6 +232,7 @@ def test_auto_reload(cmd): assert info["debug"] is False assert info["auto_reload"] is True + assert "dev" not in info @pytest.mark.parametrize( diff --git a/tests/test_exceptions_handler.py b/tests/test_exceptions_handler.py index 8be3d28e82..e9bdb21e0a 100644 --- a/tests/test_exceptions_handler.py +++ b/tests/test_exceptions_handler.py @@ -216,31 +216,6 @@ def test_exception_handler_processed_request_middleware( assert response.text == "Done." -def test_single_arg_exception_handler_notice( - exception_handler_app: Sanic, caplog: LogCaptureFixture -): - class CustomErrorHandler(ErrorHandler): - def lookup(self, exception): - return super().lookup(exception, None) - - exception_handler_app.error_handler = CustomErrorHandler() - - message = ( - "[DEPRECATION v22.3] 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." - ) - with pytest.warns(DeprecationWarning) as record: - _, response = exception_handler_app.test_client.get("/1") - - assert len(record) == 1 - assert record[0].message.args[0] == message - assert response.status == 400 - - def test_error_handler_noisy_log( exception_handler_app: Sanic, monkeypatch: MonkeyPatch ): @@ -279,7 +254,7 @@ async def handler1(request: Request): @app.route("/2") async def handler2(request: Request): - response = await request.respond() + await request.respond() raise ServerError("Exception") with caplog.at_level(logging.WARNING): From 6e3499caf97b55cc1e593ce2e99bdbfe2401c56a Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Sat, 8 Jan 2022 23:05:39 +0200 Subject: [PATCH 2/5] Exception tests --- tests/test_errorpages.py | 36 +++++++++++++++++++++++++++++------- tests/test_exceptions.py | 1 + 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/tests/test_errorpages.py b/tests/test_errorpages.py index f1e5fbd86a..042c51d6af 100644 --- a/tests/test_errorpages.py +++ b/tests/test_errorpages.py @@ -67,7 +67,7 @@ def test_auto_fallback_with_data(app): _, response = app.test_client.get("/error") assert response.status == 500 - assert response.content_type == "text/html; charset=utf-8" + assert response.content_type == "text/plain; charset=utf-8" _, response = app.test_client.post("/error", json={"foo": "bar"}) assert response.status == 500 @@ -75,7 +75,7 @@ def test_auto_fallback_with_data(app): _, response = app.test_client.post("/error", data={"foo": "bar"}) assert response.status == 500 - assert response.content_type == "text/html; charset=utf-8" + assert response.content_type == "text/plain; charset=utf-8" def test_auto_fallback_with_content_type(app): @@ -91,7 +91,7 @@ def test_auto_fallback_with_content_type(app): "/error", headers={"content-type": "foo/bar", "accept": "*/*"} ) assert response.status == 500 - assert response.content_type == "text/html; charset=utf-8" + assert response.content_type == "text/plain; charset=utf-8" def test_route_error_format_set_on_auto(app): @@ -174,6 +174,17 @@ def handler(request): ... +def test_fallback_with_content_type_html(app): + app.config.FALLBACK_ERROR_FORMAT = "auto" + + _, response = app.test_client.get( + "/error", + headers={"content-type": "application/json", "accept": "text/html"}, + ) + assert response.status == 500 + assert response.content_type == "text/html; charset=utf-8" + + def test_fallback_with_content_type_mismatch_accept(app): app.config.FALLBACK_ERROR_FORMAT = "auto" @@ -186,10 +197,10 @@ def test_fallback_with_content_type_mismatch_accept(app): _, response = app.test_client.get( "/error", - headers={"content-type": "text/plain", "accept": "foo/bar"}, + headers={"content-type": "text/html", "accept": "foo/bar"}, ) assert response.status == 500 - assert response.content_type == "text/html; charset=utf-8" + assert response.content_type == "text/plain; charset=utf-8" app.router.reset() @@ -208,7 +219,7 @@ def handler(_): headers={"accept": "foo/bar"}, ) assert response.status == 500 - assert response.content_type == "text/html; charset=utf-8" + assert response.content_type == "text/plain; charset=utf-8" _, response = app.test_client.get( "/alt1", headers={"accept": "foo/bar,*/*"}, @@ -221,7 +232,7 @@ def handler(_): headers={"accept": "foo/bar"}, ) assert response.status == 500 - assert response.content_type == "text/html; charset=utf-8" + assert response.content_type == "text/plain; charset=utf-8" _, response = app.test_client.get( "/alt2", headers={"accept": "foo/bar,*/*"}, @@ -234,6 +245,13 @@ def handler(_): headers={"accept": "foo/bar"}, ) assert response.status == 500 + assert response.content_type == "text/plain; charset=utf-8" + + _, response = app.test_client.get( + "/alt3", + headers={"accept": "foo/bar,text/html"}, + ) + assert response.status == 500 assert response.content_type == "text/html; charset=utf-8" @@ -288,6 +306,10 @@ async def start(app, _): def test_setting_fallback_on_config_changes_as_expected(app): app.error_handler = ErrorHandler() + _, response = app.test_client.get("/error") + assert response.content_type == "text/plain; charset=utf-8" + + app.config.FALLBACK_ERROR_FORMAT = "html" _, response = app.test_client.get("/error") assert response.content_type == "text/html; charset=utf-8" diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 3ec2959d38..d5b523f421 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -34,6 +34,7 @@ class SanicExceptionTestException(Exception): @pytest.fixture(scope="module") def exception_app(): app = Sanic("test_exceptions") + app.config.FALLBACK_ERROR_FORMAT = "html" @app.route("/") def handler(request): From ac3aacb48ce1e960ea9f1787fe31eb2db248a91e Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Sat, 8 Jan 2022 23:20:57 +0200 Subject: [PATCH 3/5] Remov unused import --- sanic/server/protocols/websocket_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/server/protocols/websocket_protocol.py b/sanic/server/protocols/websocket_protocol.py index aeccd973b4..2b9967c34a 100644 --- a/sanic/server/protocols/websocket_protocol.py +++ b/sanic/server/protocols/websocket_protocol.py @@ -5,7 +5,7 @@ from websockets.typing import Subprotocol from sanic.exceptions import ServerError -from sanic.log import deprecation, error_logger +from sanic.log import error_logger from sanic.server import HttpProtocol from ..websockets.impl import WebsocketImplProtocol From 5abf21cfbd488a4633e4f801a6ff3ce9b1418bdd Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Wed, 12 Jan 2022 15:02:28 +0200 Subject: [PATCH 4/5] Add AsyncMock for 3.7 --- tests/test_websockets.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_websockets.py b/tests/test_websockets.py index d129533ea4..329eff455f 100644 --- a/tests/test_websockets.py +++ b/tests/test_websockets.py @@ -1,7 +1,7 @@ import re from asyncio import Event, Queue, TimeoutError -from unittest.mock import AsyncMock, Mock, call +from unittest.mock import Mock, call import pytest @@ -11,6 +11,12 @@ from sanic.server.websockets.frame import WebsocketFrameAssembler +try: + from unittest.mock import AsyncMock +except ImportError: + from asyncmock import AsyncMock # type: ignore + + @pytest.mark.asyncio async def test_ws_frame_get_message_incomplete_timeout_0(): assembler = WebsocketFrameAssembler(Mock()) From f3baf060d505de8499b7ccd8edf0378aeadcf5f8 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Wed, 12 Jan 2022 15:02:32 +0200 Subject: [PATCH 5/5] Add AsyncMock for 3.7 --- tests/asyncmock.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/asyncmock.py diff --git a/tests/asyncmock.py b/tests/asyncmock.py new file mode 100644 index 0000000000..eec1764664 --- /dev/null +++ b/tests/asyncmock.py @@ -0,0 +1,34 @@ +""" +For 3.7 compat + +""" + + +from unittest.mock import Mock + + +class AsyncMock(Mock): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.await_count = 0 + + def __call__(self, *args, **kwargs): + self.call_count += 1 + parent = super(AsyncMock, self) + + async def dummy(): + self.await_count += 1 + return parent.__call__(*args, **kwargs) + + return dummy() + + def __await__(self): + return self().__await__() + + def assert_awaited_once(self): + if not self.await_count == 1: + msg = ( + f"Expected to have been awaited once." + f" Awaited {self.await_count} times." + ) + raise AssertionError(msg)