More consistent config setting with post-FALLBACK_ERROR_FORMAT apply (#2310)
* Update unit testing and add more consistent config * Change init and app values to private * Cleanup line lengths
This commit is contained in:
		| @@ -190,14 +190,14 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | |||||||
|         self._state: ApplicationState = ApplicationState(app=self) |         self._state: ApplicationState = ApplicationState(app=self) | ||||||
|         self.blueprints: Dict[str, Blueprint] = {} |         self.blueprints: Dict[str, Blueprint] = {} | ||||||
|         self.config: Config = config or Config( |         self.config: Config = config or Config( | ||||||
|             load_env=load_env, env_prefix=env_prefix |             load_env=load_env, | ||||||
|  |             env_prefix=env_prefix, | ||||||
|  |             app=self, | ||||||
|         ) |         ) | ||||||
|         self.configure_logging: bool = configure_logging |         self.configure_logging: bool = configure_logging | ||||||
|         self.ctx: Any = ctx or SimpleNamespace() |         self.ctx: Any = ctx or SimpleNamespace() | ||||||
|         self.debug = False |         self.debug = False | ||||||
|         self.error_handler: ErrorHandler = error_handler or ErrorHandler( |         self.error_handler: ErrorHandler = error_handler or ErrorHandler() | ||||||
|             fallback=self.config.FALLBACK_ERROR_FORMAT, |  | ||||||
|         ) |  | ||||||
|         self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list) |         self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list) | ||||||
|         self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {} |         self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {} | ||||||
|         self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {} |         self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {} | ||||||
|   | |||||||
| @@ -1,7 +1,9 @@ | |||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
| from inspect import isclass | from inspect import isclass | ||||||
| from os import environ | from os import environ | ||||||
| from pathlib import Path | 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 warnings import warn | ||||||
|  |  | ||||||
| from sanic.errorpages import check_error_format | from sanic.errorpages import check_error_format | ||||||
| @@ -9,6 +11,10 @@ from sanic.http import Http | |||||||
| from sanic.utils import load_module_from_file_location, str_to_bool | from sanic.utils import load_module_from_file_location, str_to_bool | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if TYPE_CHECKING:  # no cov | ||||||
|  |     from sanic import Sanic | ||||||
|  |  | ||||||
|  |  | ||||||
| SANIC_PREFIX = "SANIC_" | SANIC_PREFIX = "SANIC_" | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -73,10 +79,13 @@ class Config(dict): | |||||||
|         load_env: Optional[Union[bool, str]] = True, |         load_env: Optional[Union[bool, str]] = True, | ||||||
|         env_prefix: Optional[str] = SANIC_PREFIX, |         env_prefix: Optional[str] = SANIC_PREFIX, | ||||||
|         keep_alive: Optional[bool] = None, |         keep_alive: Optional[bool] = None, | ||||||
|  |         *, | ||||||
|  |         app: Optional[Sanic] = None, | ||||||
|     ): |     ): | ||||||
|         defaults = defaults or {} |         defaults = defaults or {} | ||||||
|         super().__init__({**DEFAULT_CONFIG, **defaults}) |         super().__init__({**DEFAULT_CONFIG, **defaults}) | ||||||
|  |  | ||||||
|  |         self._app = app | ||||||
|         self._LOGO = "" |         self._LOGO = "" | ||||||
|  |  | ||||||
|         if keep_alive is not None: |         if keep_alive is not None: | ||||||
| @@ -99,6 +108,7 @@ class Config(dict): | |||||||
|  |  | ||||||
|         self._configure_header_size() |         self._configure_header_size() | ||||||
|         self._check_error_format() |         self._check_error_format() | ||||||
|  |         self._init = True | ||||||
|  |  | ||||||
|     def __getattr__(self, attr): |     def __getattr__(self, attr): | ||||||
|         try: |         try: | ||||||
| @@ -106,23 +116,47 @@ class Config(dict): | |||||||
|         except KeyError as ke: |         except KeyError as ke: | ||||||
|             raise AttributeError(f"Config has no '{ke.args[0]}'") |             raise AttributeError(f"Config has no '{ke.args[0]}'") | ||||||
|  |  | ||||||
|     def __setattr__(self, attr, value): |     def __setattr__(self, attr, value) -> None: | ||||||
|         self[attr] = value |         self.update({attr: value}) | ||||||
|         if attr in ( |  | ||||||
|             "REQUEST_MAX_HEADER_SIZE", |     def __setitem__(self, attr, value) -> None: | ||||||
|             "REQUEST_BUFFER_SIZE", |         self.update({attr: value}) | ||||||
|             "REQUEST_MAX_SIZE", |  | ||||||
|         ): |     def update(self, *other, **kwargs) -> None: | ||||||
|             self._configure_header_size() |         other_mapping = {k: v for item in other for k, v in dict(item).items()} | ||||||
|         elif attr == "FALLBACK_ERROR_FORMAT": |         super().update(*other, **kwargs) | ||||||
|             self._check_error_format() |         for attr, value in {**other_mapping, **kwargs}.items(): | ||||||
|         elif attr == "LOGO": |             self._post_set(attr, value) | ||||||
|             self._LOGO = value |  | ||||||
|             warn( |     def _post_set(self, attr, value) -> None: | ||||||
|                 "Setting the config.LOGO is deprecated and will no longer " |         if self.get("_init"): | ||||||
|                 "be supported starting in v22.6.", |             if attr in ( | ||||||
|                 DeprecationWarning, |                 "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 |     @property | ||||||
|     def LOGO(self): |     def LOGO(self): | ||||||
|   | |||||||
| @@ -383,6 +383,7 @@ def exception_response( | |||||||
|     """ |     """ | ||||||
|     content_type = None |     content_type = None | ||||||
|  |  | ||||||
|  |     print("exception_response", fallback) | ||||||
|     if not renderer: |     if not renderer: | ||||||
|         # Make sure we have something set |         # Make sure we have something set | ||||||
|         renderer = base |         renderer = base | ||||||
| @@ -393,7 +394,8 @@ def exception_response( | |||||||
|             # from the route |             # from the route | ||||||
|             if request.route: |             if request.route: | ||||||
|                 try: |                 try: | ||||||
|                     render_format = request.route.ctx.error_format |                     if request.route.ctx.error_format: | ||||||
|  |                         render_format = request.route.ctx.error_format | ||||||
|                 except AttributeError: |                 except AttributeError: | ||||||
|                     ... |                     ... | ||||||
|  |  | ||||||
|   | |||||||
| @@ -918,7 +918,7 @@ class RouteMixin: | |||||||
|  |  | ||||||
|         return route |         return route | ||||||
|  |  | ||||||
|     def _determine_error_format(self, handler) -> str: |     def _determine_error_format(self, handler) -> Optional[str]: | ||||||
|         if not isinstance(handler, CompositionView): |         if not isinstance(handler, CompositionView): | ||||||
|             try: |             try: | ||||||
|                 src = dedent(getsource(handler)) |                 src = dedent(getsource(handler)) | ||||||
| @@ -930,7 +930,7 @@ class RouteMixin: | |||||||
|             except (OSError, TypeError): |             except (OSError, TypeError): | ||||||
|                 ... |                 ... | ||||||
|  |  | ||||||
|         return "auto" |         return None | ||||||
|  |  | ||||||
|     def _get_response_types(self, node): |     def _get_response_types(self, node): | ||||||
|         types = set() |         types = set() | ||||||
|   | |||||||
| @@ -139,11 +139,10 @@ class Router(BaseRouter): | |||||||
|             route.ctx.stream = stream |             route.ctx.stream = stream | ||||||
|             route.ctx.hosts = hosts |             route.ctx.hosts = hosts | ||||||
|             route.ctx.static = static |             route.ctx.static = static | ||||||
|             route.ctx.error_format = ( |             route.ctx.error_format = error_format | ||||||
|                 error_format or self.ctx.app.config.FALLBACK_ERROR_FORMAT |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|             check_error_format(route.ctx.error_format) |             if error_format: | ||||||
|  |                 check_error_format(route.ctx.error_format) | ||||||
|  |  | ||||||
|             routes.append(route) |             routes.append(route) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| from contextlib import contextmanager | from contextlib import contextmanager | ||||||
| from email import message |  | ||||||
| from os import environ | from os import environ | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from tempfile import TemporaryDirectory | from tempfile import TemporaryDirectory | ||||||
| from textwrap import dedent | from textwrap import dedent | ||||||
|  | from unittest.mock import Mock | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| @@ -360,3 +360,31 @@ def test_deprecation_notice_when_setting_logo(app): | |||||||
|     ) |     ) | ||||||
|     with pytest.warns(DeprecationWarning, match=message): |     with pytest.warns(DeprecationWarning, match=message): | ||||||
|         app.config.LOGO = "My Custom Logo" |         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) | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import pytest | |||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.errorpages import HTMLRenderer, exception_response | from sanic.errorpages import HTMLRenderer, exception_response | ||||||
| from sanic.exceptions import NotFound, SanicException | from sanic.exceptions import NotFound, SanicException | ||||||
|  | from sanic.handlers import ErrorHandler | ||||||
| from sanic.request import Request | from sanic.request import Request | ||||||
| from sanic.response import HTTPResponse, html, json, text | 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 |     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" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Adam Hopkins
					Adam Hopkins