Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON encoder change via app #2055

Merged
merged 11 commits into from
Mar 11, 2021
6 changes: 4 additions & 2 deletions sanic/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def __init__(
log_config: Optional[Dict[str, Any]] = None,
configure_logging: bool = True,
register: Optional[bool] = None,
dumps: Optional[Callable[..., str]] = None,
) -> None:
super().__init__()

Expand Down Expand Up @@ -117,8 +118,6 @@ def __init__(
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
Expand All @@ -133,6 +132,9 @@ def __init__(

self.router.ctx.app = self

if dumps:
BaseHTTPResponse._dumps = dumps

@property
def loop(self):
"""
Expand Down
10 changes: 6 additions & 4 deletions sanic/blueprint_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ def __init__(self, url_prefix=None, version=None, strict_slashes=None):
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 = []
Expand Down Expand Up @@ -90,8 +91,8 @@ def blueprints(self) -> List["sanic.Blueprint"]:
@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
"""
Expand Down Expand Up @@ -162,7 +163,8 @@ def __len__(self) -> int:

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
Expand Down
7 changes: 4 additions & 3 deletions sanic/blueprints.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -99,7 +99,8 @@ def group(*blueprints, url_prefix="", version=None, strict_slashes=None):
: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]:
Expand Down
4 changes: 2 additions & 2 deletions sanic/models/futures.py
Original file line number Diff line number Diff line change
@@ -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,
)


Expand Down
10 changes: 7 additions & 3 deletions sanic/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -66,8 +68,8 @@ def cookies(self) -> CookieJar:
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
Expand Down Expand Up @@ -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:
"""
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion tests/test_blueprint_group.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
92 changes: 92 additions & 0 deletions tests/test_json_encoding.py
Original file line number Diff line number Diff line change
@@ -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