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,
configure_logging: bool = True,
register: Optional[bool] = None,
dumps: Optional[Callable[..., str]] = None,
) -> None:
super().__init__()
@ -117,8 +118,6 @@ class Sanic(BaseSanic):
self.websocket_tasks: Set[Future] = set()
self.named_request_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_client = None
self._asgi_client = None
@ -133,6 +132,9 @@ class Sanic(BaseSanic):
self.router.ctx.app = self
if dumps:
BaseHTTPResponse._dumps = dumps
@property
def loop(self):
"""

View File

@ -61,7 +61,8 @@ class BlueprintGroup(MutableSequence):
Create a new Blueprint Group
: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
"""
self._blueprints = []
@ -90,8 +91,8 @@ class BlueprintGroup(MutableSequence):
@property
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
a version specified
API Version for the Blueprint Group. This will be applied only in case
if the Blueprint doesn't already have a version specified
:return: Version information
"""
@ -162,7 +163,8 @@ class BlueprintGroup(MutableSequence):
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
:return: Modified Blueprint

View File

@ -1,16 +1,16 @@
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.base import BaseSanic
from sanic.blueprint_group import BlueprintGroup
from sanic.models.futures import FutureRoute, FutureStatic
from sanic.models.handler_types import (
ListenerType,
MiddlewareType,
RouteHandler,
)
from sanic.models.futures import FutureRoute, FutureStatic
class Blueprint(BaseSanic):
@ -99,7 +99,8 @@ class Blueprint(BaseSanic):
:param blueprints: blueprints to be registered as a group
:param url_prefix: URL route to be prepended to all sub-prefixes
: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]:

View File

@ -1,10 +1,10 @@
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 (
ErrorMiddlewareType,
ListenerType,
MiddlewareType,
ErrorMiddlewareType,
)

View File

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

View File

@ -1,8 +1,8 @@
from pytest import raises
from sanic.app import Sanic
from sanic.blueprints import Blueprint
from sanic.blueprint_group import BlueprintGroup
from sanic.blueprints import Blueprint
from sanic.request import Request
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