From 9e7ee0a8e6937d95af2675365237f9c59a80a765 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Fri, 14 Jul 2017 03:57:11 -0600 Subject: [PATCH] Cache serialized version of Attribute, handle cache failures, legibility (#20) --- jsonattrs/__init__.py | 2 +- jsonattrs/models.py | 23 ++++++++-- tests/test_models.py | 101 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 tests/test_models.py diff --git a/jsonattrs/__init__.py b/jsonattrs/__init__.py index 872472ae..b5fb6c4e 100644 --- a/jsonattrs/__init__.py +++ b/jsonattrs/__init__.py @@ -1 +1 @@ -__version__ = '0.1.22' +__version__ = '0.1.23' diff --git a/jsonattrs/models.py b/jsonattrs/models.py index 7541a6d7..3db7553e 100644 --- a/jsonattrs/models.py +++ b/jsonattrs/models.py @@ -105,8 +105,11 @@ def save(self, *args, **kwargs): def compose_schemas(*schemas): key = 'jsonattrs:compose:' + ','.join([str(s.pk) for s in schemas]) cached = caches['jsonattrs'].get(key) - if cached is not None: - return cached + if cached: + s_attrs, required_attrs, default_attrs = cached + # Deserialize attrs when retrieving from cache + attrs = OrderedDict((k, Attribute(**v)) for k, v in s_attrs.items()) + return attrs, required_attrs, default_attrs # Extract schema attributes, names of required attributes and # names of attributes with defaults, composing schemas. @@ -123,7 +126,9 @@ def compose_schemas(*schemas): default_attrs = {n for n, a in attrs.items() if a.default is not None and a.default != ''} - caches['jsonattrs'].set(key, (attrs, required_attrs, default_attrs)) + # Serialize attrs to make it smaller in cache + s_attrs = OrderedDict((k, v.to_dict()) for k, v in attrs.items()) + caches['jsonattrs'].set(key, (s_attrs, required_attrs, default_attrs)) return attrs, required_attrs, default_attrs @@ -251,6 +256,12 @@ class Meta: objects = AttributeManager() + def __str__(self): + return "".format(self) + + def __repr__(self): + return str(self) + @property def long_name(self): if self.long_name_xlat is None or isinstance(self.long_name_xlat, str): @@ -344,3 +355,9 @@ def render(self, val): return ', '.join(result) else: return self.choice_dict.get(val, val) + + def to_dict(self): + return { + k: v for k, v in self.__dict__.items() + if k in [f.attname for f in self._meta.fields] + } diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 00000000..2774da03 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,101 @@ +from collections import OrderedDict +from django.test import TestCase +from unittest.mock import patch, MagicMock + +from jsonattrs.models import Schema, Attribute, AttributeType, compose_schemas +from .fixtures import create_fixtures + + +class ComposeSchemaTest(TestCase): + def setUp(self): + self.fixtures = create_fixtures(do_schemas=False, load_attr_types=True) + self.schema = Schema.objects.create( + content_type=self.fixtures['party_t'], selectors=() + ) + self.attr_type = AttributeType.objects.get(name='select_one') + + @patch('jsonattrs.models.caches') + def test_compose_schemas_cache_deserialize(self, mock_caches): + attr1, attr2 = Attribute.objects.bulk_create(Attribute( + id=i, + schema_id=self.schema.id, + name='testattr%s' % i, + long_name='Test attribute%s' % i, + index=i, + attr_type_id=self.attr_type.id, + required=bool(i % 2), + default=i**i if not bool(i % 2) else '') for i in range(1, 3) + ) + cache_value = ( + OrderedDict([ + ('testattr1', attr1.to_dict()), + ('testattr2', attr2.to_dict()) + ]), + {'testattr1'}, + {'testattr2'} + ) + mock_cache = MagicMock(get=MagicMock(return_value=cache_value)) + mock_caches.__getitem__.return_value = mock_cache + + assert compose_schemas(self.schema) == ( + OrderedDict([ + ('testattr1', attr1), + ('testattr2', attr2) + ]), + {'testattr1'}, + {'testattr2'} + ) + assert not mock_cache.set.called + + @patch('jsonattrs.models.caches') + def test_compose_schemas_cache_serialize(self, mock_caches): + mock_cache = MagicMock(get=MagicMock(return_value=None)) + mock_caches.__getitem__.return_value = mock_cache + + attr1, attr2 = Attribute.objects.bulk_create(Attribute( + id=i, + schema_id=self.schema.id, + name='testattr%s' % i, + long_name='Test attribute%s' % i, + index=i, + attr_type_id=self.attr_type.id, + required=bool(i % 2), + default=i**i if not bool(i % 2) else '') for i in range(1, 3) + ) + attr1.refresh_from_db() + attr2.refresh_from_db() + + assert compose_schemas(self.schema) == ( + OrderedDict([ + ('testattr1', attr1), + ('testattr2', attr2) + ]), + {'testattr1'}, + {'testattr2'} + ) + mock_cache.set.assert_called_once_with( + 'jsonattrs:compose:{}'.format(self.schema.id), + ( + OrderedDict([ + ('testattr1', attr1.to_dict()), + ('testattr2', attr2.to_dict()) + ]), + {'testattr1'}, + {'testattr2'} + ) + ) + + def test_serialize_deserialize(self): + attr = Attribute.objects.create( + schema_id=self.schema.id, + name='testattr', + long_name='Test attribute', + index=1, + attr_type_id=self.attr_type.id, + ) + assert Attribute(**attr.to_dict()) == attr + + def test_str(self): + attr = Attribute(name='testattr', id=123) + assert str(attr) == '' + assert repr(attr) == ''