JSON encoder change via app (#2055)
This commit is contained in:
parent
d76925cf35
commit
b1a57a8b62
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]:
|
||||||
|
|
|
@ -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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
92
tests/test_json_encoding.py
Normal file
92
tests/test_json_encoding.py
Normal 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
|
Loading…
Reference in New Issue
Block a user