diff --git a/dirty_models/fields.py b/dirty_models/fields.py index a37c648..07cc4cb 100644 --- a/dirty_models/fields.py +++ b/dirty_models/fields.py @@ -13,11 +13,12 @@ class BaseField: """Base field descriptor.""" - def __init__(self, name=None, alias=None, getter=None, setter=None, read_only=False, doc=None): + def __init__(self, name=None, alias=None, getter=None, setter=None, read_only=False, default=None, doc=None): self._name = None self.name = name self.alias = alias self.read_only = read_only + self.default = default self._getter = getter self._setter = setter self.__doc__ = doc or self.get_field_docstring() @@ -482,3 +483,45 @@ def convert_value(self, value): class BlobField(BaseField): pass + + +class MultiTypeField(BaseField): + + def __init__(self, field_types=None, **kwargs): + self._field_types = [] + + field_types = field_types or [] + + for field_type in field_types: + if isinstance(field_type, tuple): + field_type = field_type[0](**field_type[1]) + self._field_types.append(field_type if field_type else BaseField()) + super(MultiTypeField, self).__init__(**kwargs) + + def get_field_docstring(self): + if len(self._field_types): + return 'Multiple type values allowed:\n{0}'.format("\n".join(["* {0}".format(field.get_field_docstring()) + for field in self._field_types])) + + def export_definition(self): + result = super(MultiTypeField, self).export_definition() + result['field_types'] = [(field_type.__class__, field_type.export_definition()) + for field_type in self._field_types] + return result + + def convert_value(self, value): + for ft in self._field_types: + if ft.can_use_value(value): + return ft.convert_value(value) + + def check_value(self, value): + for ft in self._field_types: + if ft.check_value(value): + return True + return False + + def can_use_value(self, value): + for ft in self._field_types: + if ft.can_use_value(value): + return True + return False diff --git a/dirty_models/models.py b/dirty_models/models.py index 9f7ef03..73131e7 100644 --- a/dirty_models/models.py +++ b/dirty_models/models.py @@ -8,6 +8,7 @@ from datetime import datetime from collections import Mapping +from copy import deepcopy from dirty_models.base import BaseData, InnerFieldTypeMixin from dirty_models.fields import IntegerField, FloatField, BooleanField, StringField, DateTimeField @@ -35,6 +36,16 @@ def __init__(cls, name, bases, classdict): read_only_fields.append(field.name) cls._structure = structure + default_data = {} + for p in bases: + try: + default_data.update(deepcopy(p._default_data)) + except AttributeError: + pass + + default_data.update(deepcopy(cls._default_data)) + default_data.update({f.name: f.default for f in structure.values() if f.default is not None}) + cls._default_data = default_data def process_base_field(cls, field, key): """ @@ -83,6 +94,8 @@ class BaseModel(BaseData, metaclass=DirtyModelMeta): _modified_data = None _deleted_fields = None + _default_data = {} + def __init__(self, data=None, flat=False, *args, **kwargs): super(BaseModel, self).__init__(*args, **kwargs) self._original_data = {} @@ -90,6 +103,7 @@ def __init__(self, data=None, flat=False, *args, **kwargs): self._deleted_fields = [] self.unlock() + self.import_data(self._default_data) if isinstance(data, (dict, Mapping)): self.import_data(data) self.import_data(kwargs) diff --git a/tests/dirty_models/tests_fields.py b/tests/dirty_models/tests_fields.py index 92f85e9..39d7397 100644 --- a/tests/dirty_models/tests_fields.py +++ b/tests/dirty_models/tests_fields.py @@ -1,7 +1,7 @@ from unittest import TestCase from dirty_models.fields import (IntegerField, StringField, BooleanField, FloatField, ModelField, TimeField, DateField, - DateTimeField, ArrayField, StringIdField, HashMapField) + DateTimeField, ArrayField, StringIdField, HashMapField, MultiTypeField) from dirty_models.models import BaseModel, HashMapModel from dirty_models.model_types import ListModel @@ -1309,3 +1309,98 @@ def test_array_field_no_autolist(self): self.model.__class__.__dict__['array_field'].autolist = False self.model.array_field = 'foo' self.assertEqual(self.model.export_data(), {}) + + +class TestMultiTypeFieldSimpleTypes(TestCase): + + def setUp(self): + super(TestMultiTypeFieldSimpleTypes, self).setUp() + + class MultiTypeModel(BaseModel): + multi_field = MultiTypeField(field_types=[IntegerField(), StringField()]) + + self.model = MultiTypeModel() + + def test_string_field(self): + self.model.multi_field = 'foo' + self.assertEqual(self.model.multi_field, 'foo') + + def test_integer_field(self): + self.model.multi_field = 3 + self.assertEqual(self.model.multi_field, 3) + + def test_update_string_field(self): + self.model.multi_field = 3 + self.model.flat_data() + self.model.multi_field = 'foo' + self.assertEqual(self.model.multi_field, 'foo') + + def test_update_integer_field(self): + self.model.multi_field = 'foo' + self.model.flat_data() + self.model.multi_field = 3 + self.assertEqual(self.model.multi_field, 3) + + def test_no_update_integer_field(self): + self.model.multi_field = 3 + self.model.flat_data() + self.model.multi_field = [3, 4] + self.assertEqual(self.model.multi_field, 3) + + def test_integer_field_use_float(self): + self.model.multi_field = 3.0 + self.assertEqual(self.model.multi_field, 3) + + def test_string_field_conversion_priority(self): + self.model.multi_field = '3' + self.assertEqual(self.model.multi_field, '3') + + def test_multi_field_desc(self): + self.maxDiff = None + field = MultiTypeField(field_types=[IntegerField(), StringField()]) + self.assertEqual(field.export_definition(), { + 'alias': None, + 'doc': "\n".join(['Multiple type values allowed:', + '* IntegerField field', + '* StringField field']), + 'field_types': [(IntegerField, {'alias': None, + 'doc': 'IntegerField field', + 'name': None, + 'read_only': False}), + (StringField, {'alias': None, + 'doc': 'StringField field', + 'name': None, + 'read_only': False})], + 'name': None, + 'read_only': False}) + + +class TestMultiTypeFieldComplexTypes(TestCase): + + def setUp(self): + super(TestMultiTypeFieldComplexTypes, self).setUp() + + class MultiTypeModel(BaseModel): + multi_field = MultiTypeField(field_types=[IntegerField(), (ArrayField, {"field_type": StringField()})]) + + self.model = MultiTypeModel() + + def test_integer_field(self): + self.model.multi_field = 3 + self.assertEqual(self.model.multi_field, 3) + + def test_array_field(self): + self.model.multi_field = ['foo', 'bar'] + self.assertEqual(self.model.multi_field.export_data(), ['foo', 'bar']) + + def test_update_array_field(self): + self.model.multi_field = 3 + self.model.flat_data() + self.model.multi_field = ['foo', 'bar'] + self.assertEqual(self.model.multi_field.export_data(), ['foo', 'bar']) + + def test_update_integer_field(self): + self.model.multi_field = ['foo', 'bar'] + self.model.flat_data() + self.model.multi_field = 3 + self.assertEqual(self.model.multi_field, 3) diff --git a/tests/dirty_models/tests_models.py b/tests/dirty_models/tests_models.py index f7e2075..66f91a8 100644 --- a/tests/dirty_models/tests_models.py +++ b/tests/dirty_models/tests_models.py @@ -1,11 +1,11 @@ import pickle -from datetime import datetime +from datetime import datetime, date, time from unittest import TestCase from dirty_models.base import Unlocker from dirty_models.fields import (BaseField, IntegerField, FloatField, StringField, DateTimeField, ModelField, - ArrayField, BooleanField) + ArrayField, BooleanField, DateField, TimeField, HashMapField) from dirty_models.models import BaseModel, DynamicModel, HashMapModel, FastDynamicModel INITIAL_DATA = { @@ -1243,3 +1243,146 @@ def test_no_type_def(self): model = PickableHashMapModel() model.field1 = 'sdsd' self.assertEqual(model.field1, 'sdsd') + + +class SecondaryModel(BaseModel): + + field_integer = IntegerField(default=2) + field_string = StringField(default='test') + + +class ModelDefaultValues(BaseModel): + + field_integer = IntegerField(default=1) + field_string = StringField(default='foobar') + field_boolean = BooleanField(default=True) + field_float = FloatField(default=0.1) + field_date = DateField(default=date(2016, 11, 23)) + field_time = TimeField(default=time(23, 56, 59)) + field_datetime = DateTimeField(default=datetime(2016, 11, 23, 23, 56, 59)) + field_array_integer = ArrayField(field_type=IntegerField(), default=[1, 3, 4]) + field_model = ModelField(model_class=SecondaryModel, default={"field_integer": 5}) + field_hashmap = HashMapField(field_type=StringField(), default={"item1": "aaaa", + "item2": "bbbb"}) + + +class TestDefaultValues(TestCase): + + def test_field_default_value(self): + + model = ModelDefaultValues() + + self.assertEqual(model.field_integer, 1) + self.assertEqual(model.field_string, 'foobar') + self.assertEqual(model.field_boolean, True) + self.assertEqual(model.field_float, 0.1) + self.assertEqual(model.field_date, date(2016, 11, 23)) + self.assertEqual(model.field_time, time(23, 56, 59)) + self.assertEqual(model.field_datetime, datetime(2016, 11, 23, 23, 56, 59)) + self.assertEqual(model.field_array_integer.export_data(), [1, 3, 4]) + self.assertEqual(model.field_model.export_data(), {"field_integer": 5, + "field_string": "test"}) + self.assertEqual(model.field_hashmap.export_data(), {"item1": "aaaa", + "item2": "bbbb"}) + + def test_original_data(self): + model = ModelDefaultValues() + self.assertEqual(model.export_original_data(), {}) + + def test_modified_data(self): + model = ModelDefaultValues() + self.assertEqual(model.export_modified_data(), {'field_array_integer': [1, 3, 4], + 'field_boolean': True, + 'field_date': date(2016, 11, 23), + 'field_datetime': datetime(2016, 11, 23, 23, 56, 59), + 'field_float': 0.1, + 'field_hashmap': {'item1': 'aaaa', 'item2': 'bbbb'}, + 'field_integer': 1, + 'field_model': {'field_integer': 5, 'field_string': 'test'}, + 'field_string': 'foobar', + 'field_time': time(23, 56, 59)}) + + def test_modify_model(self): + model = ModelDefaultValues() + del model.field_model + del model.field_hashmap + + model.field_integer = 4 + model.field_string = 'test' + + self.assertEqual(model.export_modified_data(), {'field_array_integer': [1, 3, 4], + 'field_boolean': True, + 'field_date': date(2016, 11, 23), + 'field_datetime': datetime(2016, 11, 23, 23, 56, 59), + 'field_float': 0.1, + 'field_integer': 4, + 'field_string': 'test', + 'field_time': time(23, 56, 59)}) + + +class ModelGeneralDefault(ModelDefaultValues): + + _default_data = {'field_array_integer': [20, 30, 40], + 'field_boolean': False, + 'field_datetime': datetime(2017, 11, 23, 23, 56, 59), + 'field_float': 1.1, + 'field_hashmap': {'item3': 'cccc', 'item4': 'dddd'}, + 'field_integer': 9, + 'field_model': {'field_integer': 6, 'field_string': 'tost'}, + 'field_string': 'barfoo', + 'field_time': time(13, 56, 59)} + + +class TestGeneralDefaultValues(TestCase): + + def test_field_default_value(self): + + model = ModelGeneralDefault() + + self.assertEqual(model.field_integer, 9) + self.assertEqual(model.field_string, 'barfoo') + self.assertEqual(model.field_boolean, False) + self.assertEqual(model.field_float, 1.1) + self.assertEqual(model.field_date, date(2016, 11, 23)) + self.assertEqual(model.field_time, time(13, 56, 59)) + self.assertEqual(model.field_datetime, datetime(2017, 11, 23, 23, 56, 59)) + self.assertEqual(model.field_array_integer.export_data(), [20, 30, 40]) + self.assertEqual(model.field_model.export_data(), {"field_integer": 6, + "field_string": "tost"}) + self.assertEqual(model.field_hashmap.export_data(), {"item3": "cccc", + "item4": "dddd"}) + + def test_original_data(self): + model = ModelGeneralDefault() + self.assertEqual(model.export_original_data(), {}) + + def test_modified_data(self): + model = ModelGeneralDefault() + + self.assertEqual(model.export_modified_data(), {'field_array_integer': [20, 30, 40], + 'field_boolean': False, + 'field_date': date(2016, 11, 23), + 'field_datetime': datetime(2017, 11, 23, 23, 56, 59), + 'field_float': 1.1, + 'field_hashmap': {'item3': 'cccc', 'item4': 'dddd'}, + 'field_integer': 9, + 'field_model': {'field_integer': 6, 'field_string': 'tost'}, + 'field_string': 'barfoo', + 'field_time': time(13, 56, 59)}) + + def test_modify_model(self): + model = ModelGeneralDefault() + del model.field_model + del model.field_hashmap + + model.field_integer = 4 + model.field_string = 'test' + + self.assertEqual(model.export_modified_data(), {'field_array_integer': [20, 30, 40], + 'field_boolean': False, + 'field_date': date(2016, 11, 23), + 'field_datetime': datetime(2017, 11, 23, 23, 56, 59), + 'field_float': 1.1, + 'field_integer': 4, + 'field_string': 'test', + 'field_time': time(13, 56, 59)})