From 71d845786d1dea0ad0c9c69cd79e6c84bb3525d9 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Tue, 16 Nov 2021 13:07:33 +0200 Subject: [PATCH] Add error format commit and merge conflicts --- sanic/app.py | 20 ++++++------ sanic/config.py | 69 +++++++++++++++++++++++++++++++++------- sanic/errorpages.py | 4 ++- sanic/mixins/routes.py | 4 +-- sanic/router.py | 7 ++-- tests/test_config.py | 38 ++++++++++++++++++++++ tests/test_errorpages.py | 42 ++++++++++++++++++++++++ 7 files changed, 154 insertions(+), 30 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 0686f7ed..d24eeca0 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -173,18 +173,16 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): self.asgi = False self.auto_reload = False self.blueprints: Dict[str, Blueprint] = {} - self.config = config or Config( - load_env=load_env, env_prefix=env_prefix + self.config: Config = config or Config( + load_env=load_env, + env_prefix=env_prefix, + app=self, ) - self.configure_logging = configure_logging - self.ctx = ctx or SimpleNamespace() - self.debug = None - self.error_handler = error_handler or ErrorHandler( - fallback=self.config.FALLBACK_ERROR_FORMAT, - ) - self.is_running = False - self.is_stopping = False - self.listeners: Dict[str, List[ListenerType]] = defaultdict(list) + self.configure_logging: bool = configure_logging + self.ctx: Any = ctx or SimpleNamespace() + self.debug = False + self.error_handler: ErrorHandler = error_handler or ErrorHandler() + self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list) self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {} self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {} self.reload_dirs: Set[Path] = set() diff --git a/sanic/config.py b/sanic/config.py index 649d9414..8b9add58 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -1,7 +1,9 @@ +from __future__ import annotations + from inspect import isclass from os import environ from pathlib import Path -from typing import Any, Dict, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, Optional, Union from warnings import warn from sanic.errorpages import check_error_format @@ -10,6 +12,10 @@ from sanic.http import Http from .utils import load_module_from_file_location, str_to_bool +if TYPE_CHECKING: # no cov + from sanic import Sanic + + SANIC_PREFIX = "SANIC_" BASE_LOGO = """ @@ -71,11 +77,14 @@ class Config(dict): load_env: Optional[Union[bool, str]] = True, env_prefix: Optional[str] = SANIC_PREFIX, keep_alive: Optional[bool] = None, + *, + app: Optional[Sanic] = None, ): defaults = defaults or {} super().__init__({**DEFAULT_CONFIG, **defaults}) - self.LOGO = BASE_LOGO + self._app = app + self._LOGO = "" if keep_alive is not None: self.KEEP_ALIVE = keep_alive @@ -97,6 +106,7 @@ class Config(dict): self._configure_header_size() self._check_error_format() + self._init = True def __getattr__(self, attr): try: @@ -104,16 +114,51 @@ class Config(dict): except KeyError as ke: raise AttributeError(f"Config has no '{ke.args[0]}'") - def __setattr__(self, attr, value): - self[attr] = value - if attr in ( - "REQUEST_MAX_HEADER_SIZE", - "REQUEST_BUFFER_SIZE", - "REQUEST_MAX_SIZE", - ): - self._configure_header_size() - elif attr == "FALLBACK_ERROR_FORMAT": - self._check_error_format() + def __setattr__(self, attr, value) -> None: + self.update({attr: value}) + + def __setitem__(self, attr, value) -> None: + self.update({attr: value}) + + def update(self, *other, **kwargs) -> None: + other_mapping = {k: v for item in other for k, v in dict(item).items()} + super().update(*other, **kwargs) + for attr, value in {**other_mapping, **kwargs}.items(): + self._post_set(attr, value) + + def _post_set(self, attr, value) -> None: + if self.get("_init"): + if attr in ( + "REQUEST_MAX_HEADER_SIZE", + "REQUEST_BUFFER_SIZE", + "REQUEST_MAX_SIZE", + ): + self._configure_header_size() + elif attr == "FALLBACK_ERROR_FORMAT": + self._check_error_format() + if self.app and value != self.app.error_handler.fallback: + if self.app.error_handler.fallback != "auto": + warn( + "Overriding non-default ErrorHandler fallback " + "value. Changing from " + f"{self.app.error_handler.fallback} to {value}." + ) + self.app.error_handler.fallback = value + elif attr == "LOGO": + self._LOGO = value + warn( + "Setting the config.LOGO is deprecated and will no longer " + "be supported starting in v22.6.", + DeprecationWarning, + ) + + @property + def app(self): + return self._app + + @property + def LOGO(self): + return self._LOGO def _configure_header_size(self): Http.set_header_max_size( diff --git a/sanic/errorpages.py b/sanic/errorpages.py index 82cdd57a..d046c29d 100644 --- a/sanic/errorpages.py +++ b/sanic/errorpages.py @@ -383,6 +383,7 @@ def exception_response( """ content_type = None + print("exception_response", fallback) if not renderer: # Make sure we have something set renderer = base @@ -393,7 +394,8 @@ def exception_response( # from the route if request.route: try: - render_format = request.route.ctx.error_format + if request.route.ctx.error_format: + render_format = request.route.ctx.error_format except AttributeError: ... diff --git a/sanic/mixins/routes.py b/sanic/mixins/routes.py index 8467a2e3..7139cd3c 100644 --- a/sanic/mixins/routes.py +++ b/sanic/mixins/routes.py @@ -918,7 +918,7 @@ class RouteMixin: return route - def _determine_error_format(self, handler) -> str: + def _determine_error_format(self, handler) -> Optional[str]: if not isinstance(handler, CompositionView): try: src = dedent(getsource(handler)) @@ -930,7 +930,7 @@ class RouteMixin: except (OSError, TypeError): ... - return "auto" + return None def _get_response_types(self, node): types = set() diff --git a/sanic/router.py b/sanic/router.py index 6995ed6d..b15c2a3e 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -139,11 +139,10 @@ class Router(BaseRouter): route.ctx.stream = stream route.ctx.hosts = hosts route.ctx.static = static - route.ctx.error_format = ( - error_format or self.ctx.app.config.FALLBACK_ERROR_FORMAT - ) + route.ctx.error_format = error_format - check_error_format(route.ctx.error_format) + if error_format: + check_error_format(route.ctx.error_format) routes.append(route) diff --git a/tests/test_config.py b/tests/test_config.py index 42a7e3ec..67324f1e 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -3,6 +3,7 @@ from os import environ from pathlib import Path from tempfile import TemporaryDirectory from textwrap import dedent +from unittest.mock import Mock import pytest @@ -350,3 +351,40 @@ def test_update_from_lowercase_key(app): d = {"test_setting_value": 1} app.update_config(d) assert "test_setting_value" not in app.config + + +def test_deprecation_notice_when_setting_logo(app): + message = ( + "Setting the config.LOGO is deprecated and will no longer be " + "supported starting in v22.6." + ) + with pytest.warns(DeprecationWarning, match=message): + app.config.LOGO = "My Custom Logo" + + +def test_config_set_methods(app, monkeypatch): + post_set = Mock() + monkeypatch.setattr(Config, "_post_set", post_set) + + app.config.FOO = 1 + post_set.assert_called_once_with("FOO", 1) + post_set.reset_mock() + + app.config["FOO"] = 2 + post_set.assert_called_once_with("FOO", 2) + post_set.reset_mock() + + app.config.update({"FOO": 3}) + post_set.assert_called_once_with("FOO", 3) + post_set.reset_mock() + + app.config.update([("FOO", 4)]) + post_set.assert_called_once_with("FOO", 4) + post_set.reset_mock() + + app.config.update(FOO=5) + post_set.assert_called_once_with("FOO", 5) + post_set.reset_mock() + + app.config.update_config({"FOO": 6}) + post_set.assert_called_once_with("FOO", 6) diff --git a/tests/test_errorpages.py b/tests/test_errorpages.py index 5af4ca5f..84949fde 100644 --- a/tests/test_errorpages.py +++ b/tests/test_errorpages.py @@ -3,6 +3,7 @@ import pytest from sanic import Sanic from sanic.errorpages import HTMLRenderer, exception_response from sanic.exceptions import NotFound, SanicException +from sanic.handlers import ErrorHandler from sanic.request import Request from sanic.response import HTTPResponse, html, json, text @@ -271,3 +272,44 @@ def test_combinations_for_auto(fake_request, accept, content_type, expected): ) assert response.content_type == expected + + +def test_allow_fallback_error_format_set_main_process_start(app): + @app.main_process_start + async def start(app, _): + app.config.FALLBACK_ERROR_FORMAT = "text" + + request, response = app.test_client.get("/error") + assert request.app.error_handler.fallback == "text" + assert response.status == 500 + assert response.content_type == "text/plain; charset=utf-8" + + +def test_setting_fallback_to_non_default_raise_warning(app): + app.error_handler = ErrorHandler(fallback="text") + + assert app.error_handler.fallback == "text" + + with pytest.warns( + UserWarning, + match=( + "Overriding non-default ErrorHandler fallback value. " + "Changing from text to auto." + ), + ): + app.config.FALLBACK_ERROR_FORMAT = "auto" + + assert app.error_handler.fallback == "auto" + + app.config.FALLBACK_ERROR_FORMAT = "text" + + with pytest.warns( + UserWarning, + match=( + "Overriding non-default ErrorHandler fallback value. " + "Changing from text to json." + ), + ): + app.config.FALLBACK_ERROR_FORMAT = "json" + + assert app.error_handler.fallback == "json"