Skip to content

Commit

Permalink
Make JSON encoder extensible (#1)
Browse files Browse the repository at this point in the history
* Remove using rest framework JSONEncoder

* Using dict instead of using isinstance

* Fix type_utils.is_iterable

* add tests for encoder

* add new line to the gitignore

* add test for Enum.ID and fix JSONEncoder

* fix according to pep8

* change name of test

* fix encoder

add encoding Enum and dataclasses

* replace tests

* rename byte to bytes_

* fix typing

* change name of exception

* add inner dataclass

* delete dataclasses

* add trailing comma

* rename Inner to Nested dataclass

* use dataclasses.asdict instead of asdict

* change version to 1.0.2
  • Loading branch information
andrey-berenda authored and mofr committed Jan 30, 2019
1 parent e79faf4 commit 431ac76
Show file tree
Hide file tree
Showing 18 changed files with 269 additions and 72 deletions.
82 changes: 82 additions & 0 deletions tests/test_encodings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import datetime
import decimal
import enum
import json
import uuid

import pytest
import pytz
from dataclasses import dataclass

from winter.json_encoder import JSONEncoder


class Id(int):
pass


class Enum(enum.Enum):
ID = Id(1)
NUMBER = 1
FLOAT = 1.0
TUPLE = ('000', 1)
STRING = 'test string'


@dataclass
class NestedDataclass:
nested_number: int


@dataclass
class Dataclass:
id_: Id
number: int
string: str
date: datetime.date
nested: NestedDataclass


def get_encoder_class():
return JSONEncoder


@pytest.mark.parametrize(('value', 'expected_value'), [
(None, None),
(1, 1),
(Id(1), 1),
(Enum.ID, 1),
(Enum.NUMBER, 1),
(Enum.FLOAT, 1.0),
(Enum.TUPLE, ['000', 1]),
(Enum.STRING, 'test string'),
(datetime.datetime(year=2019, month=1, day=1, tzinfo=pytz.UTC, hour=3), '2019-01-01T03:00:00Z'),
(datetime.date(year=2019, month=1, day=1), '2019-01-01'),
(datetime.time(hour=3, minute=50, second=20), '03:50:20'),
(datetime.timedelta(hours=10, seconds=20), str(10 * 60 * 60 + 20.0)),
(decimal.Decimal(11.0), 11.0),
(uuid.UUID('c010de13-7f2d-41f9-b4f0-893087e32b92'), 'c010de13-7f2d-41f9-b4f0-893087e32b92'),
(b'test bytes', 'test bytes'),
(Dataclass(
Id(1),
1,
'test',
datetime.date(year=2019, month=1, day=1),
NestedDataclass(10),
), {'id_': 1, 'number': 1, 'string': 'test', 'date': '2019-01-01', 'nested': {'nested_number': 10}}),
])
def test_encoder(value, expected_value):
encoder_class = get_encoder_class()
data = {'key': value}
assert expected_value == json.loads(json.dumps(data, cls=encoder_class))['key']


@pytest.mark.parametrize('value', (
datetime.time(hour=3, minute=50, second=20, tzinfo=pytz.UTC),
))
def test_encoder_with_raises(value):
encoder_class = get_encoder_class()
data = {'key': value}

with pytest.raises(ValueError):
json.dumps(data, cls=encoder_class)
1 change: 1 addition & 0 deletions tests/test_type_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def test_is_iterable(typing_for_check, expected):
(Optional[int], True),
(Union[List, Tuple], True),
(Tuple, False),
(Union, True),
])
def test_is_union(typing_for_check, expected):
assert is_union(typing_for_check) == expected
Expand Down
2 changes: 0 additions & 2 deletions winter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@


def _default_configuration():
from .dataclasses import DataclassesOutputProcessorResolver
from .query_parameter import QueryParameterResolver
from .drf import DRFBodyArgumentResolver
from .drf import HttpRequestArgumentResolver
Expand All @@ -31,7 +30,6 @@ def _default_configuration():
register_argument_resolver(DRFBodyArgumentResolver())
register_argument_resolver(QueryParameterResolver())
register_argument_resolver(HttpRequestArgumentResolver())
register_output_processor_resolver(DataclassesOutputProcessorResolver())
register_controller_method_inspector(PathParametersInspector())
register_controller_method_inspector(QueryParametersInspector())

Expand Down
2 changes: 1 addition & 1 deletion winter/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.0.1'
__version__ = '1.0.2'
4 changes: 2 additions & 2 deletions winter/argument_resolver.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import typing
from abc import ABCMeta
from abc import ABC
from abc import abstractmethod
from typing import Callable
from typing import Dict
Expand All @@ -13,7 +13,7 @@
_resolvers = []


class ArgumentResolver(metaclass=ABCMeta):
class ArgumentResolver(ABC):
"""IArgumentResolver is used to map http request contents to controller method arguments."""
@abstractmethod
def is_supported(self, argument: ControllerMethodArgument) -> bool:
Expand Down
2 changes: 1 addition & 1 deletion winter/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def signature(self) -> inspect.Signature:
return inspect.signature(self.func)

def _build_arguments(self, func):
type_hints = typing.get_type_hints(self.func)
type_hints = typing.get_type_hints(func)
type_hints.pop('return', None)
arguments = {}
for arg_name, arg_type in type_hints.items():
Expand Down
20 changes: 0 additions & 20 deletions winter/dataclasses.py

This file was deleted.

2 changes: 1 addition & 1 deletion winter/django.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,6 @@ def _rewrite_uritemplate_with_regexps(winter_url_path: str, methods: List[Contro
if len(types) > 1:
raise Exception(f'Different methods are bound to the same path variable, but have different types annotated: {types}')
type_, = types
regexp = '\d+' if issubclass(type_, int) else '\w+'
regexp = r'\d+' if issubclass(type_, int) else r'\w+'
url_path = url_path.replace(f'{{{variable_name}}}', f'(?P<{variable_name}>{regexp})')
return url_path
11 changes: 6 additions & 5 deletions winter/drf/body_argument_resolver.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import django.http
from rest_framework.request import Request

from .input_serializer import get_input_serializer
from ..argument_resolver import ArgumentResolver
from ..controller import ControllerMethodArgument


class DRFBodyArgumentResolver(ArgumentResolver):

def is_supported(self, argument: ControllerMethodArgument) -> bool:
input_serializer = get_input_serializer(argument.method.func)
if not input_serializer:
if input_serializer is None:
return False
return input_serializer.destination_argument_name == argument.name

def resolve_argument(self, argument: ControllerMethodArgument, http_request: django.http.HttpRequest):
def resolve_argument(self, argument: ControllerMethodArgument, http_request: Request):
input_serializer = get_input_serializer(argument.method.func)
serializer_class = input_serializer.class_
serializer_args = input_serializer.args
serializer = serializer_class(data=http_request.data, **serializer_args)
serializer_kwargs = input_serializer.kwargs
serializer = serializer_class(data=http_request.data, **serializer_kwargs)
serializer.is_valid(raise_exception=True)
return serializer.validated_data
10 changes: 5 additions & 5 deletions winter/drf/input_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
@dataclass
class InputSerializer:
class_: Type[Serializer]
args: Dict
kwargs: Dict
destination_argument_name: str


def input_serializer(serializer_class: Type[Serializer], argument_name: str, **serializer_args):
def input_serializer(serializer_class: Type[Serializer], argument_name: str, **serializer_kwargs):
def wrapper(func):
_register_input_serializer(func, argument_name, serializer_class, serializer_args)
_register_input_serializer(func, argument_name, serializer_class, serializer_kwargs)
return func
return wrapper

Expand All @@ -26,6 +26,6 @@ def get_input_serializer(func) -> Optional[InputSerializer]:
return _input_serializers.get(func)


def _register_input_serializer(func, argument_name: str, serializer_class: Type[Serializer], serializer_args: Dict):
def _register_input_serializer(func, argument_name: str, serializer_class: Type[Serializer], serializer_kwargs: Dict):
assert func not in _input_serializers, f'{func} already has a registered input_serializer'
_input_serializers[func] = InputSerializer(serializer_class, serializer_args, argument_name)
_input_serializers[func] = InputSerializer(serializer_class, serializer_kwargs, argument_name)
6 changes: 3 additions & 3 deletions winter/drf/output_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@


class DRFOutputProcessor(IOutputProcessor):
def __init__(self, serializer_class: Type[Serializer], serializer_args: Dict):
def __init__(self, serializer_class: Type[Serializer], serializer_kwargs: Dict):
self._serializer_class = serializer_class
self._serializer_args = serializer_args
self._serializer_kwargs = serializer_kwargs

def process_output(self, output, request: Request):
if isinstance(output, BodyWithContext):
Expand All @@ -23,5 +23,5 @@ def process_output(self, output, request: Request):
instance = output
context = {}
context.update(request=request)
serializer = self._serializer_class(instance=instance, context=context, **self._serializer_args)
serializer = self._serializer_class(instance=instance, context=context, **self._serializer_kwargs)
return serializer.data
12 changes: 6 additions & 6 deletions winter/drf/output_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@
@dataclass
class OutputSerializer:
class_: Type[Serializer]
args: Dict
kwargs: Dict


_output_serializers = {}


def output_serializer(serializer_class: Type[Serializer], **serializer_args):
def output_serializer(serializer_class: Type[Serializer], **serializer_kwargs):
def wrapper(func):
output_processor = DRFOutputProcessor(serializer_class, serializer_args)
output_processor = DRFOutputProcessor(serializer_class, serializer_kwargs)
register_output_processor(func, output_processor)
_register_output_serializer(func, serializer_class, serializer_args)
_register_output_serializer(func, serializer_class, serializer_kwargs)
return func
return wrapper

Expand All @@ -31,6 +31,6 @@ def get_output_serializer(func) -> Optional[OutputSerializer]:
return _output_serializers.get(func)


def _register_output_serializer(func, serializer_class: Type[Serializer], serializer_args: Dict):
def _register_output_serializer(func, serializer_class: Type[Serializer], serializer_kwargs: Dict):
assert func not in _output_serializers
_output_serializers[func] = OutputSerializer(serializer_class, serializer_args)
_output_serializers[func] = OutputSerializer(serializer_class, serializer_kwargs)
2 changes: 2 additions & 0 deletions winter/drf/output_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@


class TemplateRenderer(IOutputProcessor):

def __init__(self, template_name: str):
self._template_name = template_name

Expand All @@ -18,4 +19,5 @@ def wrapper(func):
output_processor = TemplateRenderer(template_name)
register_output_processor(func, output_processor)
return func

return wrapper
Loading

0 comments on commit 431ac76

Please sign in to comment.