diff --git a/dataclasses_json/core.py b/dataclasses_json/core.py index 1602fef5..eb07bbab 100644 --- a/dataclasses_json/core.py +++ b/dataclasses_json/core.py @@ -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 @@ -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): @@ -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 @@ -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: @@ -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 @@ -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. @@ -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) @@ -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) \ No newline at end of file diff --git a/setup.py b/setup.py index cda81719..35054a33 100644 --- a/setup.py +++ b/setup.py @@ -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={ diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..28c8b9ad --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,3 @@ +pytest_plugins = [ + 'tests.fixtures' +] diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 00000000..9743d83f --- /dev/null +++ b/tests/fixtures.py @@ -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() diff --git a/tests/test_cache.py b/tests/test_cache.py new file mode 100644 index 00000000..447fae72 --- /dev/null +++ b/tests/test_cache.py @@ -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