JSON encoder change via app (#2055)

This commit is contained in:
Adam Hopkins 2021-03-11 17:09:18 +02:00 committed by GitHub
parent d76925cf35
commit b1a57a8b62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 116 additions and 15 deletions

View File

@ -84,6 +84,7 @@ class Sanic(BaseSanic):
log_config: Optional[Dict[str, Any]] = None, log_config: Optional[Dict[str, Any]] = None,
configure_logging: bool = True, configure_logging: bool = True,
register: Optional[bool] = None, register: Optional[bool] = None,
dumps: Optional[Callable[..., str]] = None,
) -> None: ) -> None:
super().__init__() super().__init__()
@ -117,8 +118,6 @@ class Sanic(BaseSanic):
self.websocket_tasks: Set[Future] = set() self.websocket_tasks: Set[Future] = set()
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]] = {}
# self.named_request_middleware: Dict[str, MiddlewareType] = {}
# self.named_response_middleware: Dict[str, MiddlewareType] = {}
self._test_manager = None self._test_manager = None
self._test_client = None self._test_client = None
self._asgi_client = None self._asgi_client = None
@ -133,6 +132,9 @@ class Sanic(BaseSanic):
self.router.ctx.app = self self.router.ctx.app = self
if dumps:
BaseHTTPResponse._dumps = dumps
@property @property
def loop(self): def loop(self):
""" """

View File

@ -61,7 +61,8 @@ class BlueprintGroup(MutableSequence):
Create a new Blueprint Group Create a new Blueprint Group
:param url_prefix: URL: to be prefixed before all the Blueprint Prefix :param url_prefix: URL: to be prefixed before all the Blueprint Prefix
:param version: API Version for the blueprint group. This will be inherited by each of the Blueprint :param version: API Version for the blueprint group. This will be
inherited by each of the Blueprint
:param strict_slashes: URL Strict slash behavior indicator :param strict_slashes: URL Strict slash behavior indicator
""" """
self._blueprints = [] self._blueprints = []
@ -90,8 +91,8 @@ class BlueprintGroup(MutableSequence):
@property @property
def version(self) -> Optional[Union[str, int, float]]: def version(self) -> Optional[Union[str, int, float]]:
""" """
API Version for the Blueprint Group. This will be applied only in case if the Blueprint doesn't already have API Version for the Blueprint Group. This will be applied only in case
a version specified if the Blueprint doesn't already have a version specified
:return: Version information :return: Version information
""" """
@ -162,7 +163,8 @@ class BlueprintGroup(MutableSequence):
def _sanitize_blueprint(self, bp: "sanic.Blueprint") -> "sanic.Blueprint": def _sanitize_blueprint(self, bp: "sanic.Blueprint") -> "sanic.Blueprint":
""" """
Sanitize the Blueprint Entity to override the Version and strict slash behaviors as required. Sanitize the Blueprint Entity to override the Version and strict slash
behaviors as required.
:param bp: Sanic Blueprint entity Object :param bp: Sanic Blueprint entity Object
:return: Modified Blueprint :return: Modified Blueprint

View File

@ -1,16 +1,16 @@
from collections import defaultdict from collections import defaultdict
from typing import Dict, List, Optional, Iterable from typing import Dict, Iterable, List, Optional
from sanic_routing.route import Route # type: ignore from sanic_routing.route import Route # type: ignore
from sanic.base import BaseSanic from sanic.base import BaseSanic
from sanic.blueprint_group import BlueprintGroup from sanic.blueprint_group import BlueprintGroup
from sanic.models.futures import FutureRoute, FutureStatic
from sanic.models.handler_types import ( from sanic.models.handler_types import (
ListenerType, ListenerType,
MiddlewareType, MiddlewareType,
RouteHandler, RouteHandler,
) )
from sanic.models.futures import FutureRoute, FutureStatic
class Blueprint(BaseSanic): class Blueprint(BaseSanic):
@ -99,7 +99,8 @@ class Blueprint(BaseSanic):
:param blueprints: blueprints to be registered as a group :param blueprints: blueprints to be registered as a group
:param url_prefix: URL route to be prepended to all sub-prefixes :param url_prefix: URL route to be prepended to all sub-prefixes
:param version: API Version to be used for Blueprint group :param version: API Version to be used for Blueprint group
:param strict_slashes: Indicate strict slash termination behavior for URL :param strict_slashes: Indicate strict slash termination behavior
for URL
""" """
def chain(nested) -> Iterable[Blueprint]: def chain(nested) -> Iterable[Blueprint]:

View File

@ -1,10 +1,10 @@
from pathlib import PurePath from pathlib import PurePath
from typing import NamedTuple, List, Union, Iterable, Optional from typing import Iterable, List, NamedTuple, Optional, Union
from sanic.models.handler_types import ( from sanic.models.handler_types import (
ErrorMiddlewareType,
ListenerType, ListenerType,
MiddlewareType, MiddlewareType,
ErrorMiddlewareType,
) )

View File

@ -39,6 +39,8 @@ class BaseHTTPResponse:
The base class for all HTTP Responses The base class for all HTTP Responses
""" """
_dumps = json_dumps
def __init__(self): def __init__(self):
self.asgi: bool = False self.asgi: bool = False
self.body: Optional[bytes] = None self.body: Optional[bytes] = None
@ -66,8 +68,8 @@ class BaseHTTPResponse:
response.cookies["test"]["domain"] = ".yummy-yummy-cookie.com" response.cookies["test"]["domain"] = ".yummy-yummy-cookie.com"
response.cookies["test"]["httponly"] = True response.cookies["test"]["httponly"] = True
`See user guide `See user guide re: cookies
<https://sanicframework.org/guide/basics/cookies.html>`_ <https://sanicframework.org/guide/basics/cookies.html>`__
:return: the cookie jar :return: the cookie jar
:rtype: CookieJar :rtype: CookieJar
@ -251,7 +253,7 @@ def json(
status: int = 200, status: int = 200,
headers: Optional[Dict[str, str]] = None, headers: Optional[Dict[str, str]] = None,
content_type: str = "application/json", content_type: str = "application/json",
dumps: Callable[..., str] = json_dumps, dumps: Optional[Callable[..., str]] = None,
**kwargs, **kwargs,
) -> HTTPResponse: ) -> HTTPResponse:
""" """
@ -262,6 +264,8 @@ def json(
:param headers: Custom Headers. :param headers: Custom Headers.
:param kwargs: Remaining arguments that are passed to the json encoder. :param kwargs: Remaining arguments that are passed to the json encoder.
""" """
if not dumps:
dumps = BaseHTTPResponse._dumps
return HTTPResponse( return HTTPResponse(
dumps(body, **kwargs), dumps(body, **kwargs),
headers=headers, headers=headers,

View File

@ -1,8 +1,8 @@
from pytest import raises from pytest import raises
from sanic.app import Sanic from sanic.app import Sanic
from sanic.blueprints import Blueprint
from sanic.blueprint_group import BlueprintGroup from sanic.blueprint_group import BlueprintGroup
from sanic.blueprints import Blueprint
from sanic.request import Request from sanic.request import Request
from sanic.response import HTTPResponse, text from sanic.response import HTTPResponse, text

View File

@ -0,0 +1,92 @@
import sys
from dataclasses import asdict, dataclass
from functools import partial
from json import dumps as sdumps
import pytest
try:
from ujson import dumps as udumps
NO_UJSON = False
DEFAULT_DUMPS = udumps
except ModuleNotFoundError:
NO_UJSON = True
DEFAULT_DUMPS = partial(sdumps, separators=(",", ":"))
from sanic import Sanic
from sanic.response import BaseHTTPResponse, json
@dataclass
class Foo:
bar: str
def __json__(self):
return udumps(asdict(self))
@pytest.fixture
def foo():
return Foo(bar="bar")
@pytest.fixture
def payload(foo):
return {"foo": foo}
@pytest.fixture(autouse=True)
def default_back_to_ujson():
yield
BaseHTTPResponse._dumps = DEFAULT_DUMPS
def test_change_encoder():
Sanic("...", dumps=sdumps)
assert BaseHTTPResponse._dumps == sdumps
def test_change_encoder_to_some_custom():
def my_custom_encoder():
return "foo"
Sanic("...", dumps=my_custom_encoder)
assert BaseHTTPResponse._dumps == my_custom_encoder
@pytest.mark.skipif(NO_UJSON is True, reason="ujson not installed")
def test_json_response_ujson(payload):
"""ujson will look at __json__"""
response = json(payload)
assert response.body == b'{"foo":{"bar":"bar"}}'
with pytest.raises(
TypeError, match="Object of type Foo is not JSON serializable"
):
json(payload, dumps=sdumps)
Sanic("...", dumps=sdumps)
with pytest.raises(
TypeError, match="Object of type Foo is not JSON serializable"
):
json(payload)
@pytest.mark.skipif(NO_UJSON is True, reason="ujson not installed")
def test_json_response_json():
"""One of the easiest ways to tell the difference is that ujson cannot
serialize over 64 bits"""
too_big_for_ujson = 111111111111111111111
with pytest.raises(OverflowError, match="int too big to convert"):
json(too_big_for_ujson)
response = json(too_big_for_ujson, dumps=sdumps)
assert sys.getsizeof(response.body) == 54
Sanic("...", dumps=sdumps)
response = json(too_big_for_ujson)
assert sys.getsizeof(response.body) == 54