Skip to content

Commit

Permalink
add caching, add tests (#175)
Browse files Browse the repository at this point in the history
* add caching, add tests

* revert cache_size

Co-authored-by: Charles Li <[email protected]>
  • Loading branch information
Germandrummer92 and lidatong authored Feb 23, 2020
1 parent 7d0418d commit 2208d4f
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 8 deletions.
30 changes: 23 additions & 7 deletions dataclasses_json/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
from datetime import datetime, timezone
from decimal import Decimal
from enum import Enum
from typing import Any, Collection, Mapping, Union, get_type_hints
from typing import Any, Collection, Dict, Mapping, Union, get_type_hints
from uuid import UUID

from cachetools import cached
from cachetools.lru import LRUCache
from typing_inspect import is_union_type # type: ignore

from dataclasses_json import cfg
Expand All @@ -24,6 +26,10 @@
_issubclass_safe)

Json = Union[dict, list, str, int, float, bool, None]
_MAX_CACHE_SIZE = 128
_get_type_hints_cache = LRUCache(maxsize=_MAX_CACHE_SIZE)
_is_supported_generic_cache = LRUCache(maxsize=_MAX_CACHE_SIZE)
_user_overrides_cache = LRUCache(maxsize=_MAX_CACHE_SIZE)


class _ExtendedEncoder(json.JSONEncoder):
Expand All @@ -47,10 +53,14 @@ def default(self, o) -> Json:
return result


def _user_overrides_or_exts(cls):
confs = ['encoder', 'decoder', 'mm_field', 'letter_case']
FieldOverride = namedtuple('FieldOverride', confs)
confs = ['encoder', 'decoder', 'mm_field', 'letter_case']
FieldOverride = namedtuple('FieldOverride', confs)


@cached(cache=_user_overrides_cache, key=id)
def _user_overrides_or_exts(cls) -> Dict[str, FieldOverride]:
overrides = {}
# overrides at the class-level
global_metadata = defaultdict(dict)
encoders = cfg.global_config.encoders
decoders = cfg.global_config.decoders
Expand Down Expand Up @@ -89,7 +99,7 @@ def _encode_json_type(value, default=_ExtendedEncoder().default):
return default(value)


def _encode_overrides(kvs, overrides, encode_json=False):
def _encode_overrides(kvs: Dict[str, Any], overrides, encode_json=False):
override_kvs = {}
for k, v in kvs.items():
if k in overrides:
Expand Down Expand Up @@ -118,6 +128,11 @@ def _decode_letter_case_overrides(field_names, overrides):
return names


@cached(cache=_get_type_hints_cache, key=id)
def _get_type_hints_cached(cls):
return get_type_hints(cls)


def _decode_dataclass(cls, kvs, infer_missing):
if isinstance(kvs, cls):
return kvs
Expand All @@ -140,7 +155,7 @@ def _decode_dataclass(cls, kvs, infer_missing):
kvs = _handle_undefined_parameters_safe(cls, kvs, usage="from")

init_kwargs = {}
types = get_type_hints(cls)
types = _get_type_hints_cached(cls)
for field in fields(cls):
# The field should be skipped from being added
# to init_kwargs as it's not intended as a constructor argument.
Expand Down Expand Up @@ -222,6 +237,7 @@ def _support_extended_types(field_type, field_value):
return res


@cached(cache=_is_supported_generic_cache, key=id)
def _is_supported_generic(type_):
not_str = not _issubclass_safe(type_, str)
is_enum = _issubclass_safe(type_, Enum)
Expand Down Expand Up @@ -326,4 +342,4 @@ def _asdict(obj, encode_json=False):
and not isinstance(obj, bytes):
return list(_asdict(v, encode_json=encode_json) for v in obj)
else:
return copy.deepcopy(obj)
return copy.deepcopy(obj)
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
'marshmallow>=3.3.0,<4.0.0',
'marshmallow-enum>=1.5.1,<2.0.0',
'typing-inspect>=0.4.0',
'stringcase==1.2.0,<2.0.0'
'stringcase==1.2.0,<2.0.0',
'cachetools==4.0.0,<5.0.0'
],
python_requires='>=3.6',
extras_require={
Expand Down
3 changes: 3 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pytest_plugins = [
'tests.fixtures'
]
10 changes: 10 additions & 0 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import pytest
# noinspection PyProtectedMember
from dataclasses_json.core import _get_type_hints_cache, _is_supported_generic_cache, _user_overrides_cache


@pytest.fixture(autouse=True)
def clear_caches():
_is_supported_generic_cache.clear()
_user_overrides_cache.clear()
_get_type_hints_cache.clear()
80 changes: 80 additions & 0 deletions tests/test_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from tests.entities import (DataClassWithDataClass,
DataClassWithList,
DataClassX,
DataClassXs)
# noinspection PyProtectedMember
from dataclasses_json.core import _get_type_hints_cache, _is_supported_generic_cache, _user_overrides_cache


class TestCache:

def test_dataclass_with_xs_caches_class(self):
json_dataclass_with_xs = '{"xs": [{"x": 1}]}'
_ = DataClassXs.from_json(json_dataclass_with_xs)

# type hints for class and DataClassX cached
assert _get_type_hints_cache._Cache__currsize == 2
# supported generic called for List[DataClassX] and int
assert _is_supported_generic_cache._Cache__currsize == 2
# for the dataclasses
assert _user_overrides_cache._Cache__currsize == 2

# force cache hits
_ = DataClassXs.from_json(json_dataclass_with_xs)

assert _get_type_hints_cache._Cache__currsize == 2
assert _is_supported_generic_cache._Cache__currsize == 2
assert _user_overrides_cache._Cache__currsize == 2

def test_dataclass_with_x_caches_class(self):
json_dataclass_with_x = '{"x": 1}'
_ = DataClassX.from_json(json_dataclass_with_x)

# type hints for class cached
assert _get_type_hints_cache._Cache__currsize == 1
# supported generic called for int
assert _is_supported_generic_cache._Cache__currsize == 1
# for the dataclass
assert _user_overrides_cache._Cache__currsize == 1

# force cache hits
_ = DataClassX.from_json(json_dataclass_with_x)

assert _get_type_hints_cache._Cache__currsize == 1
assert _is_supported_generic_cache._Cache__currsize == 1
assert _user_overrides_cache._Cache__currsize == 1

def test_dataclass_with_list_caches_class(self):
json_dataclass_with_list = '{"xs": [1]}'
_ = DataClassWithList.from_json(json_dataclass_with_list)

# type hints for class cached
assert _get_type_hints_cache._Cache__currsize == 1
# supported generic called for List[int] and int itself
assert _is_supported_generic_cache._Cache__currsize == 2
# for the dataclass
assert _user_overrides_cache._Cache__currsize == 1

# force cache hits
_ = DataClassWithList.from_json(json_dataclass_with_list)

assert _get_type_hints_cache._Cache__currsize == 1
assert _is_supported_generic_cache._Cache__currsize == 2
assert _user_overrides_cache._Cache__currsize == 1

def test_nested_dataclass_caches_both_classes(self):
json_dataclass_with_dataclass = '{"dc_with_list": {"xs": [1]}}'
_ = DataClassWithDataClass.from_json(json_dataclass_with_dataclass)

assert _get_type_hints_cache._Cache__currsize == 2
# supported generic called for List[int] and int
assert _is_supported_generic_cache._Cache__currsize == 2
# for the dataclasses
assert _user_overrides_cache._Cache__currsize == 2

# force cache_hit
_ = DataClassWithDataClass.from_json(json_dataclass_with_dataclass)

assert _get_type_hints_cache._Cache__currsize == 2
assert _is_supported_generic_cache._Cache__currsize == 2
assert _user_overrides_cache._Cache__currsize == 2

0 comments on commit 2208d4f

Please sign in to comment.