Skip to content

Commit

Permalink
JSON encoder change via app (#2055)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahopkins authored Mar 11, 2021
1 parent d76925c commit b1a57a8
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 15 deletions.
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

0 comments on commit b1a57a8

Please sign in to comment.