├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── drf_serializer_cache ├── __init__.py └── cache.py ├── performance.svg ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── app ├── __init__.py ├── apps.py ├── models.py └── serializers.py ├── functional ├── __init__.py ├── test_model_serializer.py └── test_serializer.py ├── performance ├── __init__.py ├── list_simple.py ├── list_with_reused_instance.py └── recursive_model_serializer.py └── settings.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # PyCharm 104 | .idea/ 105 | 106 | # pytest 107 | .pytest_cache/ 108 | 109 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | - "3.7" 5 | - "3.8" 6 | script: 7 | - pip install coveralls 8 | - python setup.py test 9 | - coveralls 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Oleg Nykolyn 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django REST framework serializers speedup 2 | 3 | [![PyPI version](https://badge.fury.io/py/drf-serializer-cache.svg)](https://badge.fury.io/py/drf-serializer-cache) 4 | [![Build Status](https://travis-ci.org/K0Te/drf-serializer-cache.svg?branch=master)](https://travis-ci.org/K0Te/drf-serializer-cache) 5 | [![Coverage Status](https://coveralls.io/repos/github/K0Te/drf-serializer-cache/badge.svg?branch=master)](https://coveralls.io/github/K0Te/drf-serializer-cache?branch=master) 6 | 7 | Simple short-living cache implementation for DRF serializers. 8 | It solves two issues with DRF serializers: 9 | * `Serializer.fields` property can be extremely slow in some cases. 10 | Such cases include complex `ModelSerializer`, serializer hierarchies 11 | with repeated serializers and recursive serializers. 12 | `fields` is cached and computed once per class for each serialization run. 13 | This leads to limitation - `fields` should not be dynamically adjusted 14 | inside of serializer, which inherits from `SerializerCacheMixin`; 15 | * By default serializers will re-compute object representation event if 16 | they encounter same object with same serializer twice. This can turn 17 | into an issue, if multiple object include same dependent hard-to-serialize 18 | objects. Cache avoid re-computing instance representation if it encounters 19 | same instances multiple times. 20 | 21 | ## Performance results 22 | ![Performance results](performance.svg) 23 | 24 | ## Cache life-time and invalidation 25 | Cache is designed to be simple and non-error-prone. 26 | Cache is created when top-level serializer `to_representation` starts 27 | and is cleaned up once this method is finished. Thus there is no need 28 | for timeouts or complex cache invalidation. No data is shared between 29 | requests. 30 | Thus, following cases will work fine: 31 | ```python 32 | user = User.objects.get(name='Lee') 33 | data_1 = UserSerializer(user).data 34 | # OrderedDict([('name', 'Lee')]) 35 | user.name = 'Bob' 36 | user.save() 37 | data_2 = UserSerializer(user).data 38 | # OrderedDict([('name', 'Bob')]) 39 | ``` 40 | 41 | ## Usage 42 | Usage should be pretty simple - inject `SerializerCacheMixin` before 43 | inheriting from `Serializer` or `ModelSerializer`: 44 | ```python 45 | from drf_serializer_cache import SerializerCacheMixin 46 | from rest_framework import serializers 47 | 48 | class UserSerializer(SerializerCacheMixin, serializers.ModelSerializer): 49 | class Meta: 50 | model = User 51 | fields = ('id', 'name') 52 | ``` 53 | 54 | ## Common pitfalls 55 | #### Too often cache cleanup 56 | Cache lives in serializer hierarchy root, but it's life-time is defined 57 | by nearest tree node, which inherits from `SerializerCacheMixin`. 58 | Ideal situation is when root inherits from `SerializerCacheMixin`. 59 | That's why `SerializerCacheMixin` uses custom `list_serializer_class`, 60 | which also inherits from `SerializerCacheMixin`. 61 | If you use custom list as root of serializer hierarchy - it's recommended 62 | to use `SerializerCacheMixin` as it's base class. 63 | #### Too many hierarchies 64 | By default serializers build nice hierarchy by calling `bind` on their 65 | child serializers. This case can be broken for `serializers.SerializerMethodField` 66 | if it uses some serializer without calling `bind` on it: 67 | ```python 68 | from drf_serializer_cache import SerializerCacheMixin 69 | from rest_framework import serializers 70 | 71 | class ResultSerializer(SerializerCacheMixin, serializers.Serializer): 72 | results = serializers.SerializerMethodField() 73 | 74 | def get_results(self, instance): 75 | # recursive serializer 76 | serializer = self.__class__(instance.results, many=True) 77 | serializer.bind('*', self) # bind call is essential for efficient cache ! 78 | return serializer.data 79 | ``` 80 | -------------------------------------------------------------------------------- /drf_serializer_cache/__init__.py: -------------------------------------------------------------------------------- 1 | """Django REST framework serializer cache.""" 2 | 3 | from .cache import SerializerCacheMixin 4 | 5 | del cache 6 | -------------------------------------------------------------------------------- /drf_serializer_cache/cache.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | 3 | from django.utils.functional import cached_property 4 | from rest_framework.serializers import ListSerializer 5 | 6 | 7 | class SerializerCacheMixin: 8 | """Mixin for DRF serializer performance improvement. 9 | 10 | Provides cache for slow "fields" property.""" 11 | 12 | @cached_property 13 | def _is_first_cachable(self): 14 | parent = self.parent 15 | while parent is not None: 16 | if isinstance(parent, SerializerCacheMixin): 17 | return False 18 | parent = parent.parent 19 | return True 20 | 21 | @contextmanager 22 | def _setup_cache(self): 23 | assert not hasattr(self.root, '_field_cache'), \ 24 | 'Double cache setup detected.' 25 | self.root._field_cache = {} 26 | assert not hasattr(self.root, '_representation_cache'), \ 27 | 'Double cache setup detected.' 28 | self.root._representation_cache = {} 29 | yield 30 | del self.root._field_cache 31 | del self.root._representation_cache 32 | 33 | def to_representation(self, instance): 34 | """Convert instance to representation with result caching.""" 35 | if self._is_first_cachable: 36 | with self._setup_cache(): 37 | return super().to_representation(instance) 38 | cache = self.root._representation_cache 39 | key = (instance, self.__class__) 40 | try: 41 | if key not in cache: 42 | cache[key] = super().to_representation(instance) 43 | return cache[key] 44 | # key might not be hashable 45 | except TypeError: 46 | return super().to_representation(instance) 47 | 48 | @property 49 | def fields(self): 50 | """Return cached fields.""" 51 | try: 52 | cache = self.root._field_cache 53 | except AttributeError: 54 | return super().fields 55 | if self.__class__ not in cache: 56 | cache[self.__class__] = super().fields 57 | return cache[self.__class__] 58 | 59 | @classmethod 60 | def many_init(cls, *args, **kwargs): 61 | """Use cached list serializer if possible.""" 62 | meta = getattr(cls, 'Meta', None) 63 | if meta is not None and not hasattr(meta, 'list_serializer_class'): 64 | # Meta has no custom list serializer, it's safe to use optimized 65 | meta.list_serializer_class = CachedListSerializer 66 | elif meta is None: 67 | cls.Meta = type( 68 | 'Meta', 69 | tuple(), 70 | {'list_serializer_class': CachedListSerializer}) 71 | return super().many_init(*args, **kwargs) 72 | 73 | 74 | class CachedListSerializer(SerializerCacheMixin, ListSerializer): 75 | pass 76 | -------------------------------------------------------------------------------- /performance.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.281.423.4681.1552.881.40.00.20.40.60.81.01.21.41.61.82.02.22.42.62.83.03.23.43.63.84.0time, seconds - less is betterList, 100% cache missList, instance reuseRecursive model  Without cacheWith cache -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | # This includes the license file in the wheel. 3 | license_file = LICENSE 4 | 5 | [bdist_wheel] 6 | # This flag says to generate wheels that support both Python 2 and Python 7 | # 3. If your code will not run unchanged on both Python 2 and 3, you will 8 | # need to generate separate wheels for each Python version that you 9 | # support. Removing this line (or setting universal to 0) will prevent 10 | # bdist_wheel from trying to make a universal wheel. For more see: 11 | # https://packaging.python.org/tutorials/distributing-packages/#wheels 12 | universal=1 13 | 14 | [aliases] 15 | test=pytest 16 | 17 | [tool:pytest] 18 | addopts = --cov=drf_serializer_cache -v 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from codecs import open 3 | from os import path 4 | 5 | here = path.abspath(path.dirname(__file__)) 6 | 7 | # Get the long description from the README file 8 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 9 | long_description = f.read() 10 | 11 | 12 | setup( 13 | name='drf-serializer-cache', 14 | version='0.3.4', 15 | description='Django REST framework (DRF) serializer speedup', 16 | license='BSD', 17 | long_description=long_description, 18 | long_description_content_type='text/markdown', 19 | url='https://github.com/K0Te/drf-serializer-cache', 20 | classifiers=[ 21 | 'Development Status :: 4 - Beta', 22 | 'Intended Audience :: Developers', 23 | 'Framework :: Django', 24 | 'License :: OSI Approved :: BSD License', 25 | 'Programming Language :: Python :: 3', 26 | 'Programming Language :: Python :: 3.4', 27 | 'Programming Language :: Python :: 3.5', 28 | 'Programming Language :: Python :: 3.6', 29 | ], 30 | keywords='drf cache performance', 31 | install_requires=['django', 'djangorestframework'], 32 | setup_requires=['pytest-runner'], 33 | tests_require=['pytest', 'pytest-cov', 'pytest-django'], 34 | test_suite='tests.functional', 35 | packages=find_packages(exclude=['tests']), # Required 36 | project_urls={ # Optional 37 | 'Bug Reports': 'https://github.com/K0Te/drf-serializer-cache/issues', 38 | 'Source': 'https://github.com/K0Te/drf-serializer-cache', 39 | }, 40 | ) 41 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K0Te/drf-serializer-cache/1126087b864342386c7ee4ef5aded51a0934f14a/tests/__init__.py -------------------------------------------------------------------------------- /tests/app/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'tests.app.apps.TestAppConfig' 2 | -------------------------------------------------------------------------------- /tests/app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TestAppConfig(AppConfig): 5 | name = 'tests.app' 6 | label = 'test-app' 7 | verbose_name = 'Test Application' 8 | -------------------------------------------------------------------------------- /tests/app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class User(models.Model): 5 | id = models.AutoField('Identifier', primary_key=True) 6 | name = models.CharField('Name', max_length=70) 7 | email = models.EmailField() 8 | 9 | 10 | class FilmCategory(models.Model): 11 | id = models.AutoField('Identifier', primary_key=True) 12 | name = models.CharField('Name', max_length=70) 13 | parent_category = models.ForeignKey('FilmCategory', 14 | null=True, 15 | default=None, 16 | on_delete=models.PROTECT) 17 | 18 | 19 | class Film(models.Model): 20 | id = models.AutoField('Identifier', primary_key=True) 21 | year = models.IntegerField('Year') 22 | name = models.CharField('Name', max_length=70) 23 | uploaded_by = models.ForeignKey(User, on_delete=models.PROTECT) 24 | category = models.ForeignKey(FilmCategory, on_delete=models.PROTECT) 25 | 26 | 27 | class CategoryHierarchy: 28 | def __init__(self, category, films=None, categories=None): 29 | self.category = category 30 | if films is None: 31 | self.films = [] 32 | else: 33 | self.films = films 34 | if categories is None: 35 | self.categories = [] 36 | else: 37 | self.categories = categories 38 | -------------------------------------------------------------------------------- /tests/app/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.serializers import ModelSerializer, Serializer 2 | from rest_framework import fields 3 | 4 | from drf_serializer_cache import SerializerCacheMixin 5 | from tests.app import models 6 | 7 | 8 | class UserSerializer(SerializerCacheMixin, ModelSerializer): 9 | class Meta: 10 | model = models.User 11 | fields = ('id', 'name') 12 | 13 | 14 | class FilmCategorySerializer(SerializerCacheMixin, ModelSerializer): 15 | class Meta: 16 | model = models.FilmCategory 17 | fields = ('id', 'name') 18 | 19 | 20 | class FilmSerializer(SerializerCacheMixin, ModelSerializer): 21 | uploaded_by = UserSerializer() 22 | category = FilmCategorySerializer() 23 | 24 | class Meta: 25 | model = models.Film 26 | fields = ('id', 'name', 'category', 'year', 'uploaded_by') 27 | 28 | 29 | class CategoryHierarchySerializer(SerializerCacheMixin, Serializer): 30 | category = FilmCategorySerializer() 31 | films = fields.SerializerMethodField() 32 | categories = fields.SerializerMethodField() 33 | 34 | def get_films(self, instance): 35 | serializer = FilmSerializer(instance.films, many=True) 36 | serializer.bind('*', self) 37 | return serializer.data 38 | 39 | def get_categories(self, instance): 40 | serializer = self.__class__(instance.categories, many=True) 41 | serializer.bind('*', self) 42 | return serializer.data 43 | -------------------------------------------------------------------------------- /tests/functional/__init__.py: -------------------------------------------------------------------------------- 1 | if __name__ == '__main__': 2 | import pytest 3 | pytest.main() 4 | -------------------------------------------------------------------------------- /tests/functional/test_model_serializer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import django 3 | 4 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' # noqa 5 | django.setup() # noqa 6 | 7 | import pytest 8 | from tests.app.serializers import UserSerializer, CategoryHierarchySerializer 9 | from tests.app import models 10 | 11 | 12 | @pytest.mark.django_db 13 | def test_simple_saved(): 14 | user = models.User.objects.create(name='Bob') 15 | 16 | serializer = UserSerializer(instance=user) 17 | data = serializer.data 18 | assert data['name'] == 'Bob' 19 | assert data['id'] > 0 20 | 21 | 22 | def test_simple_unsaved(): 23 | user = models.User(name='Bob') 24 | serializer = UserSerializer(instance=user) 25 | data = serializer.data 26 | assert data['name'] == 'Bob' 27 | assert data['id'] is None 28 | 29 | 30 | @pytest.mark.django_db 31 | def test_complex(): 32 | user = models.User.objects.create(name='Bob') 33 | top_category = models.FilmCategory.objects.create(name='All') 34 | child_movies = models.FilmCategory.objects.create( 35 | name='Child movies', parent_category=top_category) 36 | cartoons = models.FilmCategory.objects.create( 37 | name='Cartoons', parent_category=child_movies) 38 | serious_stuff = models.FilmCategory.objects.create( 39 | name='Serious', parent_category=top_category) 40 | anime = models.FilmCategory.objects.create( 41 | name='Anime', parent_category=serious_stuff) 42 | film_1 = models.Film.objects.create(name='Mickey Mouse', 43 | year=1966, 44 | uploaded_by=user, 45 | category=cartoons) 46 | film_2 = models.Film.objects.create(name='Ghost in the shell', 47 | year=1989, 48 | uploaded_by=user, 49 | category=anime) 50 | 51 | object = models.CategoryHierarchy( 52 | top_category, 53 | categories=[ 54 | models.CategoryHierarchy( 55 | child_movies, 56 | categories=[ 57 | models.CategoryHierarchy( 58 | cartoons, 59 | films=[film_1] 60 | ) 61 | ] 62 | ), 63 | models.CategoryHierarchy( 64 | serious_stuff, 65 | categories=[ 66 | models.CategoryHierarchy( 67 | anime, 68 | films=[film_2] 69 | ) 70 | ] 71 | ), 72 | ]) 73 | serializer = CategoryHierarchySerializer(object) 74 | data = serializer.data 75 | assert data['category']['name'] == 'All' 76 | assert data['categories'][0]['category']['name'] == 'Child movies' 77 | assert data['categories'][0]['categories'][0]['films'][0]['year'] == 1966 78 | assert data['categories'][1]['category']['name'] == 'Serious' 79 | assert data['categories'][1]['categories'][0]['films'][0]['year'] == 1989 80 | -------------------------------------------------------------------------------- /tests/functional/test_serializer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import django 3 | 4 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' # noqa 5 | django.setup() # noqa 6 | 7 | from collections import namedtuple 8 | from rest_framework.serializers import Serializer 9 | from rest_framework import fields 10 | from drf_serializer_cache import SerializerCacheMixin 11 | 12 | 13 | class PointSerializer(SerializerCacheMixin, Serializer): 14 | x = fields.IntegerField() 15 | y = fields.IntegerField(source='xx') 16 | 17 | 18 | def test_single_point(): 19 | serializer = PointSerializer({'x': 1, 'xx': 123}) 20 | assert serializer.data['x'] == 1 21 | assert serializer.data['y'] == 123 22 | 23 | 24 | def test_point_list(): 25 | serializer = PointSerializer([{'x': 1, 'xx': 123}, 26 | {'x': 123, 'xx': 456}], 27 | many=True) 28 | assert serializer.data[0]['x'] == 1 29 | assert serializer.data[0]['y'] == 123 30 | assert serializer.data[1]['x'] == 123 31 | assert serializer.data[1]['y'] == 456 32 | 33 | 34 | def test_point_same_element(): 35 | class CountedSerializer(Serializer): 36 | def __init__(self, *args, **kwargs): 37 | super().__init__(*args, **kwargs) 38 | self.call_count = 0 39 | 40 | def to_representation(self, instance): 41 | self.call_count += 1 42 | res = super().to_representation(instance) 43 | res.update({'count': self.call_count}) 44 | return res 45 | 46 | class CountedPointSerializer(CountedSerializer): 47 | x = fields.IntegerField() 48 | y = fields.IntegerField() 49 | 50 | class CachedCountedPointSerializer(SerializerCacheMixin, CountedSerializer): 51 | x = fields.IntegerField() 52 | y = fields.IntegerField() 53 | 54 | point_cls = namedtuple('Point', 'x y') 55 | p1 = point_cls(1, 2) 56 | p2 = point_cls(3, 4) 57 | 58 | serializer = CountedPointSerializer([p1, p2, p1, p2], many=True) 59 | assert serializer.data[0]['x'] == 1 60 | assert serializer.data[0]['y'] == 2 61 | assert serializer.data[0]['count'] == 1 62 | assert serializer.data[-1]['x'] == 3 63 | assert serializer.data[-1]['y'] == 4 64 | # non-cached variant yields 4 65 | assert serializer.data[-1]['count'] == 4 66 | 67 | serializer = CachedCountedPointSerializer([p1, p2, p1, p2], many=True) 68 | assert serializer.data[0]['x'] == 1 69 | assert serializer.data[0]['y'] == 2 70 | assert serializer.data[0]['count'] == 1 71 | assert serializer.data[-1]['x'] == 3 72 | assert serializer.data[-1]['y'] == 4 73 | # cached variant yields 2 - as there are two distinct objects 74 | assert serializer.data[-1]['count'] == 2 75 | 76 | 77 | def test_root_non_cachable(): 78 | class RootSerializer(Serializer): 79 | points = PointSerializer(many=True) 80 | 81 | serializer = RootSerializer( 82 | {'points': [{'x': 1, 'xx': 123}, {'x': 123, 'xx': 456}]}) 83 | assert serializer.data['points'][0]['x'] == 1 84 | assert serializer.data['points'][0]['y'] == 123 85 | assert serializer.data['points'][1]['x'] == 123 86 | assert serializer.data['points'][1]['y'] == 456 87 | 88 | 89 | def test_validate_serializer(): 90 | p = PointSerializer(data={'x': 1, 'y': 123}) 91 | p.is_valid(raise_exception=True) 92 | 93 | data = p.validated_data 94 | assert data['x'] == 1 95 | assert data['xx'] == 123 96 | -------------------------------------------------------------------------------- /tests/performance/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K0Te/drf-serializer-cache/1126087b864342386c7ee4ef5aded51a0934f14a/tests/performance/__init__.py -------------------------------------------------------------------------------- /tests/performance/list_simple.py: -------------------------------------------------------------------------------- 1 | import os 2 | import django 3 | import sys 4 | sys.path.append(os.getcwd()) 5 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' # noqa 6 | django.setup() # noqa 7 | 8 | from rest_framework.serializers import Serializer 9 | from rest_framework import fields 10 | 11 | from drf_serializer_cache import SerializerCacheMixin 12 | 13 | 14 | class Point: 15 | def __init__(self, x, y, z): 16 | self.x = x 17 | self.y = y 18 | self.z = z 19 | 20 | 21 | points = [Point(x, y, z) 22 | for x in range(10) 23 | for y in range(10) 24 | for z in range(10)] 25 | 26 | 27 | class PointSerializer(Serializer): 28 | x = fields.IntegerField() 29 | y = fields.IntegerField() 30 | z = fields.IntegerField() 31 | 32 | 33 | class CachedPointSerializer(SerializerCacheMixin, PointSerializer): 34 | pass 35 | 36 | 37 | def simple_serialize(): 38 | return PointSerializer(points, many=True).data 39 | 40 | 41 | def cached_serialize(): 42 | return CachedPointSerializer(points, many=True).data 43 | 44 | 45 | if __name__ == '__main__': 46 | import timeit 47 | assert simple_serialize() == cached_serialize(), 'Result is wrong' 48 | print('Simple list serializer without cache: ', 49 | timeit.timeit('simple_serialize()', 50 | setup='from __main__ import simple_serialize', 51 | number=100)) 52 | print('Simple list serializer with cache(100% cache miss): ', 53 | timeit.timeit('cached_serialize()', 54 | setup='from __main__ import cached_serialize', 55 | number=100)) 56 | -------------------------------------------------------------------------------- /tests/performance/list_with_reused_instance.py: -------------------------------------------------------------------------------- 1 | import os 2 | import django 3 | import sys 4 | sys.path.append(os.getcwd()) 5 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' # noqa 6 | django.setup() # noqa 7 | 8 | from rest_framework.serializers import Serializer 9 | from rest_framework import fields 10 | 11 | from drf_serializer_cache import SerializerCacheMixin 12 | 13 | 14 | class Point: 15 | def __init__(self, x, y, z): 16 | self.x = x 17 | self.y = y 18 | self.z = z 19 | 20 | 21 | class Line: 22 | def __init__(self, start, end): 23 | self.start = start 24 | self.end = end 25 | 26 | 27 | class PointSerializer(Serializer): 28 | x = fields.IntegerField() 29 | y = fields.IntegerField() 30 | z = fields.IntegerField() 31 | 32 | 33 | class CachedPointSerializer(SerializerCacheMixin, PointSerializer): 34 | 35 | class Meta: 36 | """Just some empty meta info.""" 37 | 38 | 39 | class LineSerializer(Serializer): 40 | start = PointSerializer() 41 | end = PointSerializer() 42 | 43 | 44 | class CachedLineSerializer(SerializerCacheMixin, Serializer): 45 | start = CachedPointSerializer() 46 | end = CachedPointSerializer() 47 | 48 | class Meta: 49 | """Just some empty meta info.""" 50 | 51 | 52 | start_point = Point(0, 0, 0) 53 | end_point = Point(10, 10, 10) 54 | lines = [Line(start_point, end_point) for _ in range(1000)] 55 | 56 | 57 | def simple_serialize(): 58 | return LineSerializer(lines, many=True).data 59 | 60 | 61 | def cached_serialize(): 62 | return CachedLineSerializer(lines, many=True).data 63 | 64 | 65 | if __name__ == '__main__': 66 | import timeit 67 | assert simple_serialize() == cached_serialize(), 'Result is wrong' 68 | print('Simple list serializer without cache: ', 69 | timeit.timeit('simple_serialize()', 70 | setup='from __main__ import simple_serialize', 71 | number=100)) 72 | print('Simple list serializer with cache: ', 73 | timeit.timeit('cached_serialize()', 74 | setup='from __main__ import cached_serialize', 75 | number=100)) 76 | -------------------------------------------------------------------------------- /tests/performance/recursive_model_serializer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import django 3 | import timeit 4 | import sys 5 | sys.path.append(os.getcwd()) 6 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' # noqa 7 | django.setup() # noqa 8 | 9 | from rest_framework.serializers import Serializer, ModelSerializer 10 | from rest_framework import fields 11 | from tests.app import models 12 | from tests.app.serializers import CategoryHierarchySerializer as CachedHierarchy 13 | from django.db import connection 14 | 15 | 16 | class UserSerializer(ModelSerializer): 17 | class Meta: 18 | model = models.User 19 | fields = ('id', 'name') 20 | 21 | 22 | class FilmCategorySerializer(ModelSerializer): 23 | class Meta: 24 | model = models.FilmCategory 25 | fields = ('id', 'name') 26 | 27 | 28 | class FilmSerializer(ModelSerializer): 29 | uploaded_by = UserSerializer() 30 | category = FilmCategorySerializer() 31 | 32 | class Meta: 33 | model = models.Film 34 | fields = ('id', 'name', 'category', 'year', 'uploaded_by') 35 | 36 | 37 | class CategoryHierarchySerializer(Serializer): 38 | category = FilmCategorySerializer() 39 | films = fields.SerializerMethodField() 40 | categories = fields.SerializerMethodField() 41 | 42 | def get_films(self, instance): 43 | serializer = FilmSerializer(instance.films, many=True) 44 | serializer.bind('*', self) 45 | return serializer.data 46 | 47 | def get_categories(self, instance): 48 | serializer = self.__class__(instance.categories, many=True) 49 | serializer.bind('*', self) 50 | return serializer.data 51 | 52 | def main(): 53 | user = models.User.objects.create(name='Bob') 54 | top_category = models.FilmCategory.objects.create(name='All') 55 | child_movies = models.FilmCategory.objects.create( 56 | name='Child movies', parent_category=top_category) 57 | cartoons = models.FilmCategory.objects.create( 58 | name='Cartoons', parent_category=child_movies) 59 | serious_stuff = models.FilmCategory.objects.create( 60 | name='Serious', parent_category=top_category) 61 | anime = models.FilmCategory.objects.create( 62 | name='Anime', parent_category=serious_stuff) 63 | object = models.CategoryHierarchy( 64 | top_category, 65 | categories=[ 66 | models.CategoryHierarchy( 67 | child_movies, 68 | categories=[ 69 | models.CategoryHierarchy( 70 | cartoons, 71 | films=[ 72 | models.Film.objects.create(name='Mickey Mouse', 73 | year=1966, 74 | uploaded_by=user, 75 | category=cartoons) 76 | for _ in range(10) 77 | ] 78 | ) 79 | ] 80 | ), 81 | models.CategoryHierarchy( 82 | serious_stuff, 83 | categories=[ 84 | models.CategoryHierarchy( 85 | anime, 86 | films=[ 87 | models.Film.objects.create(name='Ghost in the shell', 88 | year=1989, 89 | uploaded_by=user, 90 | category=anime) 91 | for _ in range(10) 92 | ] 93 | ) 94 | ] 95 | ), 96 | ]) 97 | 98 | def simple_serialize(): 99 | return CategoryHierarchySerializer(object).data 100 | 101 | def cached_serialize(): 102 | return CachedHierarchy(object).data 103 | 104 | assert simple_serialize() == cached_serialize(), 'Result is wrong' 105 | print('Model with recursion serializer: ', 106 | timeit.timeit('simple_serialize()', 107 | globals={'simple_serialize': simple_serialize}, 108 | number=500)) 109 | print('Cached model with recursion serializer: ', 110 | timeit.timeit('cached_serialize()', 111 | globals={'cached_serialize': cached_serialize}, 112 | number=500)) 113 | 114 | 115 | if __name__ == '__main__': 116 | connection.creation.create_test_db() 117 | main() 118 | connection.creation.destroy_test_db() 119 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | """Test project settings.""" 2 | 3 | SECRET_KEY = 'tes$t' 4 | INSTALLED_APPS = [ 5 | 'rest_framework', 6 | 'tests.app' 7 | ] 8 | 9 | DATABASES = { 10 | 'default': { 11 | 'ENGINE': 'django.db.backends.sqlite3', 12 | 'NAME': ':memory:', 13 | } 14 | } 15 | --------------------------------------------------------------------------------