From 4b4a0131fe351db498495d9539b6d493f197f0a1 Mon Sep 17 00:00:00 2001 From: alfred82santa Date: Mon, 17 Oct 2016 20:25:12 +0200 Subject: [PATCH 01/10] Add new Enum field & tests --- dirty_models/fields.py | 59 ++++++++++++++++++++++++++++-- tests/dirty_models/tests_fields.py | 54 ++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 5 deletions(-) diff --git a/dirty_models/fields.py b/dirty_models/fields.py index 3f33505..c2c2260 100644 --- a/dirty_models/fields.py +++ b/dirty_models/fields.py @@ -11,7 +11,7 @@ __all__ = ['IntegerField', 'FloatField', 'BooleanField', 'StringField', 'StringIdField', 'TimeField', 'DateField', 'DateTimeField', 'TimedeltaField', 'ModelField', 'ArrayField', - 'HashMapField', 'BlobField', 'MultiTypeField'] + 'HashMapField', 'BlobField', 'MultiTypeField', 'EnumField'] class BaseField: @@ -124,7 +124,7 @@ def check_value(self, value): def can_use_value(self, value): return isinstance(value, float) \ - or (isinstance(value, str) and value.isdigit()) + or (isinstance(value, str) and value.isdigit()) class FloatField(BaseField): @@ -146,7 +146,7 @@ def check_value(self, value): def can_use_value(self, value): return isinstance(value, int) or \ - (isinstance(value, str) and + (isinstance(value, str) and value.replace('.', '', 1).isnumeric()) @@ -607,6 +607,7 @@ def __set__(self, obj, value): class InnerFieldTypeMixin: + def __init__(self, field_type=None, **kwargs): self._field_type = None if isinstance(field_type, tuple): @@ -787,3 +788,55 @@ def get_field_type_by_value(self, value): @property def field_types(self): return self._field_types.copy() + + +class EnumField(BaseField): + """ + It allows to create a field which contains a member of an enumeration. + + **Automatic cast from:** + + * Any value of enumeration. + + * Any member name of enumeration. + """ + + def __init__(self, enum_class, *args, **kwargs): + """ + + :param enum_class: Enumeration class + :type enum_class: enum.Enum + """ + self.enum_class = enum_class + super(EnumField, self).__init__(*args, **kwargs) + + def export_definition(self): + result = super(EnumField, self).export_definition() + result['enum_class'] = self.enum_class + + return result + + def get_field_docstring(self): + dcstr = super(EnumField, self).get_field_docstring() + + if self.enum_class: + dcstr += ' (:class:`{0}`)'.format('.'.join([self.enum_class.__module__, self.enum_class.__name__])) + return dcstr + + def convert_value(self, value): + try: + return self.enum_class(value) + except ValueError: + return getattr(self.enum_class, value) + + def check_value(self, value): + return isinstance(value, self.enum_class) + + def can_use_value(self, value): + try: + self.enum_class(value) + return True + except ValueError: + pass + + return value in self.enum_class.__members__.keys() diff --git a/tests/dirty_models/tests_fields.py b/tests/dirty_models/tests_fields.py index d6ecdef..33ecef6 100644 --- a/tests/dirty_models/tests_fields.py +++ b/tests/dirty_models/tests_fields.py @@ -1,4 +1,5 @@ from datetime import time, date, datetime, timezone, timedelta +from enum import Enum from unittest import TestCase import iso8601 @@ -6,7 +7,8 @@ from dirty_models.fields import (IntegerField, StringField, BooleanField, FloatField, ModelField, TimeField, DateField, - DateTimeField, ArrayField, StringIdField, HashMapField, MultiTypeField, TimedeltaField) + DateTimeField, ArrayField, StringIdField, HashMapField, MultiTypeField, TimedeltaField, + EnumField) from dirty_models.model_types import ListModel from dirty_models.models import BaseModel, HashMapModel @@ -1531,7 +1533,7 @@ def test_export_definition(self): field.export_definition()) -class TimeFieldWithTimezone(TestCase): +class TimeFieldWithTimezoneTests(TestCase): def test_no_timezone_none(self): class Model(BaseModel): @@ -1581,3 +1583,51 @@ def test_export_definition(self): 'default_timezone': timezone.utc, 'name': 'test_field', 'read_only': False}, field.export_definition()) + + +class EnumFieldTests(TestCase): + + class TestEnum(Enum): + + value_1 = 'value1' + value_2 = 2 + + def setUp(self): + self.field = EnumField(name='test_field', alias=[], enum_class=self.TestEnum) + + def test_check_value(self): + self.assertTrue(self.field.check_value(self.TestEnum.value_1)) + self.assertTrue(self.field.check_value(self.TestEnum.value_2)) + + def test_check_value_fail(self): + self.assertFalse(self.field.check_value('value_1')) + self.assertFalse(self.field.check_value(2)) + + def test_can_use_value_check_values(self): + self.assertTrue(self.field.can_use_value('value1')) + self.assertTrue(self.field.can_use_value(2)) + + def test_can_use_value_check_member_names(self): + self.assertTrue(self.field.can_use_value('value_1')) + self.assertTrue(self.field.can_use_value('value_2')) + + def test_can_use_value_check_values_fail(self): + self.assertFalse(self.field.can_use_value('value2')) + self.assertFalse(self.field.can_use_value(3)) + + def test_convert_value_from_values(self): + self.assertEqual(self.field.convert_value('value1'), self.TestEnum.value_1) + self.assertEqual(self.field.convert_value(2), self.TestEnum.value_2) + + def test_convert_value_from_member_names(self): + self.assertEqual(self.field.convert_value('value_1'), self.TestEnum.value_1) + self.assertEqual(self.field.convert_value('value_2'), self.TestEnum.value_2) + + def test_export_definition(self): + self.assertEqual(self.field.export_definition(), + {'alias': [], + 'doc': 'EnumField field (:class:`{0}`)'.format('.'.join([self.TestEnum.__module__, + self.TestEnum.__name__])), + 'enum_class': self.TestEnum, + 'name': 'test_field', 'read_only': False}, + self.field.export_definition()) From 96c693b271e1bb03a74bf052da8a4a4d2b260ec2 Mon Sep 17 00:00:00 2001 From: alfred82santa Date: Sat, 29 Oct 2016 23:47:27 +0200 Subject: [PATCH 02/10] Better distribution --- .travis.yml | 4 +++- README.rst | 14 +++++++++++--- dirty_models/fields.py | 27 ++++++++++++++++----------- dirty_models/models.py | 18 +++++++++++------- requirements-docs.txt | 1 - requirements-py33.txt | 1 + requirements-test.txt | 3 +-- requirements.txt | 1 + setup.py | 22 +++++++++++++++++++--- 9 files changed, 63 insertions(+), 28 deletions(-) create mode 100644 requirements-py33.txt diff --git a/.travis.yml b/.travis.yml index 712979c..c000fa8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,10 @@ python: - "3.4" - "3.5" # command to install dependencies -install: +install: + - make requirements - pip install -r requirements-test.txt + - if [[ $TRAVIS_PYTHON_VERSION == 3.3.* ]]; then pip install -r requirements-py33.txt --use-mirrors; fi - pip install coveralls # command to run tests script: diff --git a/README.rst b/README.rst index 7512526..512dcbc 100644 --- a/README.rst +++ b/README.rst @@ -85,6 +85,14 @@ Features Changelog --------- +Version 0.9.0 +------------- + +- New EnumField. +- Fixes on setup.py. +- Fixes on requirements. + + Version 0.8.1 ------------- @@ -101,10 +109,10 @@ Version 0.8.0 - Raise a RunTimeError exception if two fields use same alias in a model. - Fixed default docstrings. - Cleanup default data. Only real name fields are allowed to use as key. -- Added :meth:`~dirty_models.models.get_attrs_by_path` in order to get all values using path. -- Added :meth:`~dirty_models.models.get_1st_attr_by_path` in order to get first value using path. +- Added :meth:`~dirty_models.models.BaseModel.get_attrs_by_path` in order to get all values using path. +- Added :meth:`~dirty_models.models.BaseModel.get_1st_attr_by_path` in order to get first value using path. - Added option to access fields like in a dictionary, but using wildcards. Only for getters. - See: :meth:`~dirty_models.models.get_1st_attr_by_path`. + See: :meth:`~dirty_models.models.BaseModel.get_1st_attr_by_path`. - Added some documentation. Version 0.7.2 diff --git a/dirty_models/fields.py b/dirty_models/fields.py index c2c2260..55fddd5 100644 --- a/dirty_models/fields.py +++ b/dirty_models/fields.py @@ -3,6 +3,7 @@ """ from datetime import datetime, date, time, timedelta +from enum import Enum from collections import Mapping from dateutil.parser import parse as dateutil_parse @@ -79,28 +80,33 @@ def delete_value(self, obj): """Removes field value from model""" obj.delete_field_value(self.name) + def _check_name(self): + if self._name is None: + raise AttributeError("Field name must be set") + def __get__(self, obj, cls=None): if obj is None: return self + + self._check_name() + if self._getter: return self._getter(self, obj, cls) - if self._name is None: - raise AttributeError("Field name must be set") + return self.get_value(obj) def __set__(self, obj, value): + self._check_name() + if self._setter: self._setter(self, obj, value) return - if self._name is None: - raise AttributeError("Field name must be set") if self.check_value(value) or self.can_use_value(value): self.set_value(obj, self.use_value(value)) def __delete__(self, obj): - if self._name is None: - raise AttributeError("Field name must be set") + self._check_name() self.delete_value(obj) @@ -124,7 +130,7 @@ def check_value(self, value): def can_use_value(self, value): return isinstance(value, float) \ - or (isinstance(value, str) and value.isdigit()) + or (isinstance(value, str) and value.isdigit()) class FloatField(BaseField): @@ -145,9 +151,9 @@ def check_value(self, value): return isinstance(value, float) def can_use_value(self, value): - return isinstance(value, int) or \ - (isinstance(value, str) and - value.replace('.', '', 1).isnumeric()) + return isinstance(value, int) \ + or (isinstance(value, str) and + value.replace('.', '', 1).isnumeric()) class BooleanField(BaseField): @@ -607,7 +613,6 @@ def __set__(self, obj, value): class InnerFieldTypeMixin: - def __init__(self, field_type=None, **kwargs): self._field_type = None if isinstance(field_type, tuple): diff --git a/dirty_models/models.py b/dirty_models/models.py index f254bcc..bfe876b 100644 --- a/dirty_models/models.py +++ b/dirty_models/models.py @@ -4,11 +4,12 @@ import itertools from datetime import datetime, date, time, timedelta +from enum import Enum from collections import Mapping from copy import deepcopy -from dirty_models.fields import DateField, TimeField, TimedeltaField +from dirty_models.fields import DateField, TimeField, TimedeltaField, EnumField from .base import BaseData, InnerFieldTypeMixin from .fields import IntegerField, FloatField, BooleanField, StringField, DateTimeField, \ BaseField, ModelField, ArrayField @@ -165,14 +166,15 @@ def __init__(self, data=None, flat=False, *args, **kwargs): BaseModel.__setattr__(self, '__modified_data__', {}) BaseModel.__setattr__(self, '__deleted_fields__', []) - self.unlock() - self.import_data(self.__default_data__) - if isinstance(data, (dict, Mapping)): - self.import_data(data) - self.import_data(kwargs) + from .base import Unlocker + with Unlocker(self): + self.import_data(self.__default_data__) + if isinstance(data, (dict, Mapping)): + self.import_data(data) + self.import_data(kwargs) + if flat: self.flat_data() - self.lock() def __reduce__(self): """ @@ -684,6 +686,8 @@ def _get_field_type(self, key, value): return DateField(name=key) elif isinstance(value, timedelta): return TimedeltaField(name=key) + elif isinstance(value, Enum): + return EnumField(enum_class=type(value)) elif isinstance(value, (dict, BaseDynamicModel, Mapping)): return ModelField(name=key, model_class=self._dynamic_model or self.__class__) elif isinstance(value, BaseModel): diff --git a/requirements-docs.txt b/requirements-docs.txt index c72c2a9..3064c7e 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,3 +1,2 @@ sphinx -python-dateutil iso8601 \ No newline at end of file diff --git a/requirements-py33.txt b/requirements-py33.txt new file mode 100644 index 0000000..37eb288 --- /dev/null +++ b/requirements-py33.txt @@ -0,0 +1 @@ +enum34 \ No newline at end of file diff --git a/requirements-test.txt b/requirements-test.txt index 6db9492..74416d7 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,6 +1,5 @@ -pep8 < 1.6.0 +pep8 flake8 coverage nose -python-dateutil iso8601 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e69de29..4ea05ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1 @@ +python-dateutil \ No newline at end of file diff --git a/setup.py b/setup.py index 8ccb5f5..c0ac6f0 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,28 @@ -from setuptools import setup +import sys + import os +import re +from setuptools import setup + +install_requires = ['python-dateutil'] + +if sys.version_info < (3, 4): + install_requires.append('enum34') + +with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as desc_file: + long_desc = desc_file.read() + +invalid_roles = ['meth', 'class'] + +long_desc = re.sub(r':({}):`([^`]+)`'.format('|'.join(invalid_roles)), r'``\2``', long_desc, re.M) setup( name='dirty-models', url='https://github.com/alfred82santa/dirty-models', author='alfred82santa', - version='0.8.1', + version='0.9.0', author_email='alfred82santa@gmail.com', + license='BSD', classifiers=[ 'Intended Audience :: Developers', 'Programming Language :: Python', @@ -19,7 +35,7 @@ include_package_data=False, install_requires=['python-dateutil'], description="Dirty models for python 3", - long_description=open(os.path.join(os.path.dirname(__file__), 'README.rst')).read(), + long_description=long_desc, test_suite="nose.collector", tests_require="nose", zip_safe=True, From e2a80aa5d1a2571cfecdf9aae200ccc055094baa Mon Sep 17 00:00:00 2001 From: alfred82santa Date: Sat, 29 Oct 2016 23:50:25 +0200 Subject: [PATCH 03/10] Upss --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index c0ac6f0..d92a3c3 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ import sys - import os import re from setuptools import setup @@ -33,7 +32,7 @@ 'Development Status :: 4 - Beta'], packages=['dirty_models'], include_package_data=False, - install_requires=['python-dateutil'], + install_requires=install_requires, description="Dirty models for python 3", long_description=long_desc, test_suite="nose.collector", From 70dac53a18fd31f7ee81c1dc3fc3f3f29393a450 Mon Sep 17 00:00:00 2001 From: alfred82santa Date: Mon, 31 Oct 2016 16:22:40 +0100 Subject: [PATCH 04/10] Fixes for use enums & tests --- Makefile | 2 +- README.rst | 2 + dirty_models/fields.py | 68 ++++++++++++++++++++ dirty_models/models.py | 38 +++++------- dirty_models/utils.py | 44 ++++++------- tests/dirty_models/tests_fields.py | 99 ++++++++++++++++++++---------- tests/dirty_models/tests_models.py | 16 ++++- tests/dirty_models/tests_utils.py | 46 +++++++++----- 8 files changed, 220 insertions(+), 95 deletions(-) diff --git a/Makefile b/Makefile index 1f3997b..ea0dd49 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ requirements-docs: run-tests: @echo "Running tests..." - nosetests --with-coverage -d --cover-package=dirty_models --cover-erase + nosetests --with-coverage -d --cover-package=dirty_models --cover-erase -x publish: @echo "Publishing new version on Pypi..." diff --git a/README.rst b/README.rst index 512dcbc..d386dad 100644 --- a/README.rst +++ b/README.rst @@ -91,6 +91,8 @@ Version 0.9.0 - New EnumField. - Fixes on setup.py. - Fixes on requirements. +- Fixes on formatter iters. +- Fixes on code. Version 0.8.1 diff --git a/dirty_models/fields.py b/dirty_models/fields.py index 55fddd5..5766efe 100644 --- a/dirty_models/fields.py +++ b/dirty_models/fields.py @@ -7,6 +7,7 @@ from collections import Mapping from dateutil.parser import parse as dateutil_parse +from functools import wraps from .model_types import ListModel @@ -110,6 +111,40 @@ def __delete__(self, obj): self.delete_value(obj) +def can_use_enum(func): + """ + Decorator to use Enum value on type checks. + """ + + @wraps(func) + def inner(self, value): + if isinstance(value, Enum): + return self.check_value(value.value) or func(self, value.value) + + return func(self, value) + + return inner + + +def convert_enum(func): + """ + Decorator to use Enum value on type casts. + """ + + @wraps(func) + def inner(self, value): + try: + if self.check_value(value.value): + return value.value + return func(self, value.value) + except AttributeError: + pass + + return func(self, value) + + return inner + + class IntegerField(BaseField): """ It allows to use an integer as value in a field. @@ -120,14 +155,18 @@ class IntegerField(BaseField): * :class:`str` if all characters are digits + * :class:`~enum.Enum` if value of enum can be cast. + """ + @convert_enum def convert_value(self, value): return int(value) def check_value(self, value): return isinstance(value, int) + @can_use_enum def can_use_value(self, value): return isinstance(value, float) \ or (isinstance(value, str) and value.isdigit()) @@ -142,14 +181,18 @@ class FloatField(BaseField): * :class:`int` * :class:`str` if all characters are digits and there is only one dot (``.``). + + * :class:`~enum.Enum` if value of enum can be cast. """ + @convert_enum def convert_value(self, value): return float(value) def check_value(self, value): return isinstance(value, float) + @can_use_enum def can_use_value(self, value): return isinstance(value, int) \ or (isinstance(value, str) and @@ -165,8 +208,11 @@ class BooleanField(BaseField): * :class:`int` ``0`` become ``False``, anything else ``True`` * :class:`str` ``true`` and ``yes`` become ``True``, anything else ``False``. It is case-insensitive. + + * :class:`~enum.Enum` if value of enum can be cast. """ + @convert_enum def convert_value(self, value): if isinstance(value, str): if value.lower().strip() in ['true', 'yes']: @@ -179,6 +225,7 @@ def convert_value(self, value): def check_value(self, value): return isinstance(value, bool) + @can_use_enum def can_use_value(self, value): return isinstance(value, (int, str)) @@ -193,14 +240,18 @@ class StringField(BaseField): * :class:`int` * :class:`float` + + * :class:`~enum.Enum` if value of enum can be cast. """ + @convert_enum def convert_value(self, value): return str(value) def check_value(self, value): return isinstance(value, str) + @can_use_enum def can_use_value(self, value): return isinstance(value, (int, float)) @@ -215,6 +266,8 @@ class StringIdField(StringField): * :class:`int` * :class:`float` + + * :class:`~enum.Enum` if value of enum can be cast. """ def set_value(self, obj, value): @@ -334,6 +387,8 @@ class TimeField(DateTimeBaseField): * :class:`int` will be used as timestamp. * :class:`~datetime.datetime` will get time part. + + * :class:`~enum.Enum` if value of enum can be cast. """ def __init__(self, parse_format=None, default_timezone=None, **kwargs): @@ -354,6 +409,7 @@ def __init__(self, parse_format=None, default_timezone=None, **kwargs): super(TimeField, self).__init__(parse_format=parse_format, **kwargs) self.default_timezone = default_timezone + @convert_enum def convert_value(self, value): if isinstance(value, list): return time(*value) @@ -376,6 +432,7 @@ def convert_value(self, value): def check_value(self, value): return isinstance(value, time) + @can_use_enum def can_use_value(self, value): return isinstance(value, (int, str, datetime, list, dict)) @@ -408,8 +465,11 @@ class DateField(DateTimeBaseField): * :class:`int` will be used as timestamp. * :class:`~datetime.datetime` will get date part. + + * :class:`~enum.Enum` if value of enum can be cast. """ + @convert_enum def convert_value(self, value): if isinstance(value, list): return date(*value) @@ -432,6 +492,7 @@ def convert_value(self, value): def check_value(self, value): return type(value) is date + @can_use_enum def can_use_value(self, value): return isinstance(value, (int, str, datetime, list, dict)) @@ -451,6 +512,8 @@ class DateTimeField(DateTimeBaseField): * :class:`int` will be used as timestamp. * :class:`~datetime.date` will set date part. + + * :class:`~enum.Enum` if value of enum can be cast. """ def __init__(self, parse_format=None, default_timezone=None, force_timezone=False, **kwargs): @@ -477,6 +540,7 @@ def __init__(self, parse_format=None, default_timezone=None, force_timezone=Fals self.default_timezone = default_timezone self.force_timezone = force_timezone + @convert_enum def convert_value(self, value): if isinstance(value, list): return datetime(*value) @@ -499,6 +563,7 @@ def convert_value(self, value): def check_value(self, value): return type(value) is datetime + @can_use_enum def can_use_value(self, value): return isinstance(value, (int, str, date, dict, list)) @@ -529,8 +594,10 @@ class TimedeltaField(BaseField): * :class:`int` as seconds. + * :class:`~enum.Enum` if value of enum can be cast. """ + @convert_enum def convert_value(self, value): if isinstance(value, (int, float)): return timedelta(seconds=value) @@ -538,6 +605,7 @@ def convert_value(self, value): def check_value(self, value): return type(value) is timedelta + @can_use_enum def can_use_value(self, value): return isinstance(value, (int, float)) diff --git a/dirty_models/models.py b/dirty_models/models.py index bfe876b..0a7cb52 100644 --- a/dirty_models/models.py +++ b/dirty_models/models.py @@ -184,7 +184,7 @@ def __reduce__(self): return recover_model_from_data, (self.__class__, self.export_original_data(), self.export_modified_data(), self.export_deleted_fields(),) - def _get_real_name(self, name): + def get_real_name(self, name): obj = self.get_field_obj(name) try: return obj.name @@ -195,7 +195,7 @@ def set_field_value(self, name, value): """ Set the value to the field modified_data """ - name = self._get_real_name(name) + name = self.get_real_name(name) if name and self._can_write_field(name): if name in self.__deleted_fields__: @@ -216,7 +216,7 @@ def get_field_value(self, name): """ Get the field value from the modified data or the original one """ - name = self._get_real_name(name) + name = self.get_real_name(name) if not name or name in self.__deleted_fields__: return None @@ -229,7 +229,7 @@ def delete_field_value(self, name): """ Mark this field to be deleted """ - name = self._get_real_name(name) + name = self.get_real_name(name) if name and self._can_write_field(name): if name in self.__modified_data__: @@ -242,7 +242,7 @@ def reset_field_value(self, name): """ Resets value of a field """ - name = self._get_real_name(name) + name = self.get_real_name(name) if name and self._can_write_field(name): if name in self.__modified_data__: @@ -260,7 +260,7 @@ def is_modified_field(self, name): """ Returns whether a field is modified or not """ - name = self._get_real_name(name) + name = self.get_real_name(name) if name in self.__modified_data__ or name in self.__deleted_fields__: return True @@ -359,7 +359,7 @@ def get_original_field_value(self, name): """ Returns original field value or None """ - name = self._get_real_name(name) + name = self.get_real_name(name) try: value = self.__original_data__[name] @@ -517,9 +517,7 @@ def __contains__(self, item): @classmethod def get_field_obj(cls, name): obj_field = getattr(cls, name, None) - if not isinstance(obj_field, BaseField): - return None - return obj_field + return obj_field if isinstance(obj_field, BaseField) else None def _get_fields_by_path(self, field): @@ -687,7 +685,7 @@ def _get_field_type(self, key, value): elif isinstance(value, timedelta): return TimedeltaField(name=key) elif isinstance(value, Enum): - return EnumField(enum_class=type(value)) + return EnumField(name=key, enum_class=type(value)) elif isinstance(value, (dict, BaseDynamicModel, Mapping)): return ModelField(name=key, model_class=self._dynamic_model or self.__class__) elif isinstance(value, BaseModel): @@ -807,11 +805,12 @@ def __reduce__(self): (self.get_field_type().__class__, self.get_field_type().export_definition())) - def _get_real_name(self, name): - new_name = super(HashMapModel, self)._get_real_name(name) - if not new_name: - return name - return new_name + def get_real_name(self, name): + new_name = super(HashMapModel, self).get_real_name(name) + return new_name if new_name else name + + def get_field_obj(self, name): + return super(HashMapModel, self).get_field_obj(name) or self._field_type def copy(self): """ @@ -899,11 +898,8 @@ def __init__(self, *args, **kwargs): self._dynamic_model = FastDynamicModel super(FastDynamicModel, self).__init__(*args, **kwargs) - def _get_real_name(self, name): - new_name = super(FastDynamicModel, self)._get_real_name(name) - if not new_name: - return name - return new_name + def get_real_name(self, name): + return super(FastDynamicModel, self).get_real_name(name) or name def get_validated_object(self, field_type, value): """ diff --git a/dirty_models/utils.py b/dirty_models/utils.py index 3380fab..eb3a04f 100644 --- a/dirty_models/utils.py +++ b/dirty_models/utils.py @@ -1,7 +1,10 @@ -import re -from json.encoder import JSONEncoder as BaseJSONEncoder from datetime import date, datetime, time, timedelta -from .fields import MultiTypeField, DateTimeBaseField +from enum import Enum +from json.encoder import JSONEncoder as BaseJSONEncoder + +import re + +from .fields import MultiTypeField from .model_types import ListModel from .models import BaseModel, HashMapModel @@ -18,7 +21,6 @@ class BaseFormatterIter: class BaseFieldtypeFormatterIter(BaseFormatterIter): - def __init__(self, obj, field, parent_formatter): self.obj = obj self.field = field @@ -26,20 +28,11 @@ def __init__(self, obj, field, parent_formatter): class ListFormatterIter(BaseFieldtypeFormatterIter): - def __iter__(self): for item in self.obj: yield self.parent_formatter.format_field(self.field, item) -class HashMapFormatterIter(BaseFieldtypeFormatterIter): - - def __iter__(self): - for fieldname in self.obj.get_fields(): - value = self.obj.get_field_value(fieldname) - yield fieldname, self.parent_formatter.format_field(self.field, value) - - class BaseModelFormatterIter(BaseFormatterIter): """ Base formatter iterator for Dirty Models. @@ -52,32 +45,34 @@ def __iter__(self): fields = self.model.get_fields() for fieldname in fields: field = self.model.get_field_obj(fieldname) - yield field.name, self.format_field(field, - self.model.get_field_value(fieldname)) + name = self.model.get_real_name(fieldname) + yield name, self.format_field(field, + self.model.get_field_value(fieldname)) def format_field(self, field, value): if isinstance(field, MultiTypeField): return self.format_field(field.get_field_type_by_value(value), value) - elif isinstance(value, HashMapModel): - return HashMapFormatterIter(obj=value, field=value.get_field_type(), parent_formatter=self) elif isinstance(value, BaseModel): return self.__class__(value) elif isinstance(value, ListModel): return ListFormatterIter(obj=value, field=value.get_field_type(), parent_formatter=self) + elif isinstance(value, Enum): + return self.format_field(field, value.value) return value class ModelFormatterIter(BaseModelFormatterIter): - """ Iterate over model fields formatting them. """ def format_field(self, field, value): - if isinstance(value, (date, datetime, time)) and \ - isinstance(field, DateTimeBaseField): - return field.get_formatted_value(value) + if isinstance(value, (date, datetime, time)) and not isinstance(field, MultiTypeField): + try: + return field.get_formatted_value(value) + except AttributeError: + return str(value) elif isinstance(value, timedelta): return value.total_seconds() @@ -85,15 +80,16 @@ def format_field(self, field, value): class JSONEncoder(BaseJSONEncoder): - """ Json encoder for Dirty Models """ + default_model_iter = ModelFormatterIter + def default(self, obj): if isinstance(obj, BaseModel): - return {k: v for k, v in ModelFormatterIter(obj)} - elif isinstance(obj, (HashMapFormatterIter, ModelFormatterIter)): + return {k: v for k, v in self.default_model_iter(obj)} + elif isinstance(obj, (BaseModelFormatterIter)): return {k: v for k, v in obj} elif isinstance(obj, ListFormatterIter): return list(obj) diff --git a/tests/dirty_models/tests_fields.py b/tests/dirty_models/tests_fields.py index 33ecef6..5d74f70 100644 --- a/tests/dirty_models/tests_fields.py +++ b/tests/dirty_models/tests_fields.py @@ -15,35 +15,6 @@ class TestFields(TestCase): - def test_int_field_using_int(self): - field = IntegerField() - self.assertTrue(field.check_value(3)) - self.assertEqual(field.use_value(3), 3) - - def test_int_field_desc(self): - field = IntegerField() - self.assertEqual(field.export_definition(), {'alias': None, - 'doc': 'IntegerField field', - 'name': None, - 'read_only': False}) - - def test_int_field_using_float(self): - field = IntegerField() - self.assertFalse(field.check_value(3.0)) - self.assertTrue(field.can_use_value(3.0)) - self.assertEqual(field.use_value(3.0), 3) - - def test_int_field_using_str(self): - field = IntegerField() - self.assertFalse(field.check_value("3")) - self.assertTrue(field.can_use_value("3")) - self.assertEqual(field.use_value("3"), 3) - - def test_int_field_using_dict(self): - field = IntegerField() - self.assertFalse(field.check_value({})) - self.assertFalse(field.can_use_value({})) - def test_float_field_using_int(self): field = FloatField() self.assertFalse(field.check_value(3)) @@ -1298,6 +1269,72 @@ def test_array_field_no_autolist(self): self.assertEqual(self.model.export_data(), {}) +class IntegerFieldFieldTests(TestCase): + + class TestEnum(Enum): + value_1 = 1 + value_2 = '2' + value_3 = 3.2 + value_4 = 'value' + + def test_using_int(self): + field = IntegerField() + self.assertTrue(field.check_value(3)) + self.assertEqual(field.use_value(3), 3) + + def test_desc(self): + field = IntegerField() + self.assertEqual(field.export_definition(), {'alias': None, + 'doc': 'IntegerField field', + 'name': None, + 'read_only': False}) + + def test_using_float(self): + field = IntegerField() + self.assertFalse(field.check_value(3.0)) + self.assertTrue(field.can_use_value(3.0)) + self.assertEqual(field.use_value(3.0), 3) + + def test_using_str(self): + field = IntegerField() + self.assertFalse(field.check_value("3")) + self.assertTrue(field.can_use_value("3")) + self.assertEqual(field.use_value("3"), 3) + + def test_using_dict(self): + field = IntegerField() + self.assertFalse(field.check_value({})) + self.assertFalse(field.can_use_value({})) + + def test_using_int_enum(self): + field = IntegerField() + self.assertFalse(field.check_value(self.TestEnum.value_1)) + self.assertTrue(field.can_use_value(self.TestEnum.value_1)) + self.assertEqual(field.use_value(self.TestEnum.value_1), 1) + + def test_using_str_enum(self): + field = IntegerField() + self.assertFalse(field.check_value(self.TestEnum.value_2)) + self.assertTrue(field.can_use_value(self.TestEnum.value_2)) + self.assertEqual(field.use_value(self.TestEnum.value_2), 2) + + def test_using_float_enum(self): + field = IntegerField() + self.assertFalse(field.check_value(self.TestEnum.value_3)) + self.assertTrue(field.can_use_value(self.TestEnum.value_3)) + self.assertEqual(field.use_value(self.TestEnum.value_3), 3) + + def test_using_str_enum_fail(self): + field = IntegerField() + self.assertFalse(field.check_value(self.TestEnum.value_4)) + self.assertFalse(field.can_use_value(self.TestEnum.value_4)) + + def test_using_enum_fail(self): + field = IntegerField() + self.assertFalse(field.check_value(self.TestEnum)) + self.assertFalse(field.can_use_value(self.TestEnum)) + + class MultiTypeFieldSimpleTypesTests(TestCase): def setUp(self): @@ -1407,10 +1444,10 @@ def test_get_field_type_by_value_fail(self): multi_field.get_field_type_by_value({}) -class AutoreferenceModelTests(TestCase): +class AutoreferenceModelFieldTests(TestCase): def setUp(self): - super(AutoreferenceModelTests, self).setUp() + super(AutoreferenceModelFieldTests, self).setUp() class AutoreferenceModel(BaseModel): multi_field = MultiTypeField(field_types=[IntegerField(), (ArrayField, {"field_type": ModelField()})]) diff --git a/tests/dirty_models/tests_models.py b/tests/dirty_models/tests_models.py index 612bae7..e3e85e6 100644 --- a/tests/dirty_models/tests_models.py +++ b/tests/dirty_models/tests_models.py @@ -1,12 +1,15 @@ import pickle from datetime import datetime, date, time, timedelta +from enum import Enum + from functools import partial from unittest import TestCase from dirty_models.base import Unlocker from dirty_models.fields import (BaseField, IntegerField, FloatField, StringField, DateTimeField, ModelField, - ArrayField, BooleanField, DateField, TimeField, HashMapField, TimedeltaField) + ArrayField, BooleanField, DateField, TimeField, HashMapField, TimedeltaField, + EnumField) from dirty_models.models import BaseModel, DynamicModel, HashMapModel, FastDynamicModel, CamelCaseMeta INITIAL_DATA = { @@ -917,7 +920,7 @@ def tearDown(self): def _get_field_type(self, name): try: - return self.model.__class__.__dict__[name] + return self.model.get_field_obj(name) except KeyError: return None @@ -995,6 +998,15 @@ def test_set_list_value(self): self.assertIsInstance(self._get_field_type('test1'), ArrayField) self.assertIsInstance(self._get_field_type('test1').field_type, StringField) + def test_set_enum_value(self): + class TestEnum(Enum): + value_1 = 1 + + self.model.test1 = TestEnum.value_1 + self.assertEqual(self.model.export_data(), {"test1": TestEnum.value_1}) + self.assertIsInstance(self._get_field_type('test1'), EnumField) + self.assertEqual(self._get_field_type('test1').enum_class, TestEnum) + def test_set_empty_list_value(self): self.model.test1 = [] diff --git a/tests/dirty_models/tests_utils.py b/tests/dirty_models/tests_utils.py index 2342698..467525b 100644 --- a/tests/dirty_models/tests_utils.py +++ b/tests/dirty_models/tests_utils.py @@ -1,16 +1,15 @@ from datetime import datetime, date, timedelta +from enum import Enum from json import dumps, loads from unittest.case import TestCase from dirty_models.fields import StringIdField, IntegerField, DateTimeField, ArrayField, MultiTypeField, ModelField, \ - HashMapField, DateField, TimedeltaField + HashMapField, DateField, TimedeltaField, EnumField from dirty_models.models import BaseModel, DynamicModel, FastDynamicModel -from dirty_models.utils import underscore_to_camel, ModelFormatterIter, ListFormatterIter, HashMapFormatterIter, \ - JSONEncoder +from dirty_models.utils import underscore_to_camel, ModelFormatterIter, ListFormatterIter, JSONEncoder class UnderscoreToCamelTests(TestCase): - def test_no_underscore(self): self.assertEqual(underscore_to_camel('foobar'), 'foobar') @@ -28,6 +27,10 @@ def test_underscore_multi_number(self): class TestModel(BaseModel): + class TestEnum(Enum): + value_1 = 1 + value_2 = '2' + value_3 = date(year=2015, month=7, day=30) test_string_field_1 = StringIdField(name='other_field') test_int_field_1 = IntegerField() @@ -36,16 +39,17 @@ class TestModel(BaseModel): test_array_multitype = ArrayField(field_type=MultiTypeField(field_types=[IntegerField(), DateTimeField( parse_format="%Y-%m-%dT%H:%M:%S" - )])) + )])) test_model_field_1 = ArrayField(field_type=ArrayField(field_type=ModelField())) test_hash_map = HashMapField(field_type=DateField(parse_format="%Y-%m-%d date")) test_timedelta = TimedeltaField() + test_enum = EnumField(enum_class=TestEnum) + test_multi_field = MultiTypeField(field_types=[IntegerField(), + DateField(parse_format="%Y-%m-%d multi date")]) class ModelFormatterIterTests(TestCase): - def test_model_formatter(self): - model = TestModel(data={'test_string_field_1': 'foo', 'test_int_field_1': 4, 'test_datetime': datetime(year=2016, month=5, day=30, @@ -60,7 +64,9 @@ def test_model_formatter(self): 'test_model_field_1': [[{'test_datetime': datetime(year=2015, month=7, day=30, hour=22, minute=22, second=22)}]], 'test_hash_map': {'foo': date(year=2015, month=7, day=30)}, - 'test_timedelta': timedelta(seconds=32.1122)}) + 'test_timedelta': timedelta(seconds=32.1122), + 'test_enum': TestModel.TestEnum.value_3, + 'test_multi_field': date(year=2015, month=7, day=30)}) formatter = ModelFormatterIter(model) data = {k: v for k, v in formatter} @@ -75,9 +81,11 @@ def test_model_formatter(self): self.assertIsInstance(list(data['test_model_field_1'])[0], ListFormatterIter) self.assertEqual({k: v for k, v in list(list(data['test_model_field_1'])[0])[0]}, {'test_datetime': '2015-07-30T22:22:22'}) - self.assertIsInstance(data['test_hash_map'], HashMapFormatterIter) + self.assertIsInstance(data['test_hash_map'], ModelFormatterIter) self.assertEqual({k: v for k, v in data['test_hash_map']}, {'foo': '2015-07-30 date'}) self.assertEqual(data['test_timedelta'], 32.1122) + self.assertEqual(data['test_enum'], str(date(year=2015, month=7, day=30))) + self.assertEqual(data['test_multi_field'], '2015-07-30 multi date') def test_dynamic_model_formatter(self): model = DynamicModel(data={'test_string_field_1': 'foo', @@ -91,7 +99,9 @@ def test_dynamic_model_formatter(self): 'test_model_field_1': [[{'test_datetime': datetime(year=2015, month=7, day=30, hour=22, minute=22, second=22)}]], 'test_hash_map': {'foo': date(year=2015, month=7, day=30)}, - 'test_timedelta': timedelta(seconds=32.1122)}) + 'test_timedelta': timedelta(seconds=32.1122), + 'test_enum': TestModel.TestEnum.value_1, + 'test_multi_field': date(year=2015, month=7, day=30)}) formatter = ModelFormatterIter(model) data = {k: v for k, v in formatter} @@ -121,7 +131,9 @@ def test_fast_dynamic_model_formatter(self): hour=22, minute=22, second=22)}]], 'test_hash_map': {'foo': date(year=2015, month=7, day=30)}, - 'test_timedelta': timedelta(seconds=32.1122)}) + 'test_timedelta': timedelta(seconds=32.1122), + 'test_enum': TestModel.TestEnum.value_1, + 'test_multi_field': date(year=2015, month=7, day=30)}) formatter = ModelFormatterIter(model) data = {k: v for k, v in formatter} @@ -136,12 +148,11 @@ def test_fast_dynamic_model_formatter(self): self.assertIsInstance(data['test_hash_map'], ModelFormatterIter) self.assertEqual({k: v for k, v in data['test_hash_map']}, {'foo': '2015-07-30'}) self.assertEqual(data['test_timedelta'], 32.1122) + self.assertEqual(data['test_multi_field'], '2015-07-30') class JSONEncoderTests(TestCase): - def test_model_json(self): - model = TestModel(data={'test_string_field_1': 'foo', 'test_int_field_1': 4, 'test_datetime': datetime(year=2016, month=5, day=30, @@ -156,7 +167,9 @@ def test_model_json(self): 'test_model_field_1': [[{'test_datetime': datetime(year=2015, month=7, day=30, hour=22, minute=22, second=22)}]], 'test_hash_map': {'foo': date(year=2015, month=7, day=30)}, - 'test_timedelta': timedelta(seconds=32.1122)}) + 'test_timedelta': timedelta(seconds=32.1122), + 'test_enum': TestModel.TestEnum.value_1, + 'test_multi_field': date(year=2015, month=7, day=30)}) json_str = dumps(model, cls=JSONEncoder) @@ -168,12 +181,13 @@ def test_model_json(self): 'test_array_multitype': ['2015-05-30T22:22:22', 4, 5], 'test_model_field_1': [[{'test_datetime': '2015-07-30T22:22:22'}]], 'test_hash_map': {'foo': '2015-07-30 date'}, - 'test_timedelta': 32.1122} + 'test_timedelta': 32.1122, + 'test_enum': 1, + 'test_multi_field': '2015-07-30 multi date'} self.assertEqual(loads(json_str), data) def test_general_use_json(self): - data = {'foo': 3, 'bar': 'str'} json_str = dumps(data, cls=JSONEncoder) self.assertEqual(loads(json_str), data) From ae00e01094a8650387592d7a47a4f87f5eba4a1a Mon Sep 17 00:00:00 2001 From: alfred82santa Date: Mon, 31 Oct 2016 16:31:43 +0100 Subject: [PATCH 05/10] Fix version on docs & flake8 --- dirty_models/utils.py | 2 +- docs/source/conf.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dirty_models/utils.py b/dirty_models/utils.py index eb3a04f..789fe62 100644 --- a/dirty_models/utils.py +++ b/dirty_models/utils.py @@ -6,7 +6,7 @@ from .fields import MultiTypeField from .model_types import ListModel -from .models import BaseModel, HashMapModel +from .models import BaseModel def underscore_to_camel(string): diff --git a/docs/source/conf.py b/docs/source/conf.py index 1b36daa..d11e9ad 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -65,9 +65,9 @@ # built documents. # # The short X.Y version. -version = '0.7.0' +version = '0.9.0' # The full version, including alpha/beta/rc tags. -release = '0.7.0' +release = '0.9.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 6fa4282102516d5f72e46b8fcd32e0a8cf99536a Mon Sep 17 00:00:00 2001 From: alfred82santa Date: Wed, 2 Nov 2016 20:28:43 +0100 Subject: [PATCH 06/10] Share version --- README.rst | 2 ++ dirty_models/__init__.py | 2 ++ docs/source/conf.py | 9 +++++---- setup.py | 5 ++++- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index d386dad..923e36c 100644 --- a/README.rst +++ b/README.rst @@ -93,6 +93,8 @@ Version 0.9.0 - Fixes on requirements. - Fixes on formatter iters. - Fixes on code. +- Added ``__version__`` to main package file. +- Synchronized version between main packege file, ``setup.py`` and docs. Version 0.8.1 diff --git a/dirty_models/__init__.py b/dirty_models/__init__.py index 0c40a86..3dec7f4 100644 --- a/dirty_models/__init__.py +++ b/dirty_models/__init__.py @@ -6,3 +6,5 @@ from .models import * from .fields import * + +__version__ = '0.9.0' \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index d11e9ad..6221e0d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -15,7 +15,7 @@ import sys import os - +from datetime import datetime dirty_models_path = os.path.join(os.path.dirname(__file__), '..', '..') sys.path.insert(0, dirty_models_path) # If extensions (or modules to document with autodoc) are in another directory, @@ -57,17 +57,18 @@ # General information about the project. project = 'Dirty Models' -copyright = '2016, alfred82santa, tmarques82, padajuan, xejarque, oarnau' +copyright = '{}, alfred82santa, tmarques82, padajuan, xejarque, oarnau'.format(datetime.now().year) author = 'alfred82santa, tmarques82, padajuan, xejarque, oarnau' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # +from dirty_models import __version__ # The short X.Y version. -version = '0.9.0' +version = __version__ # The full version, including alpha/beta/rc tags. -release = '0.9.0' +release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index d92a3c3..974b920 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,11 @@ import sys + import os import re from setuptools import setup +import dirty_models + install_requires = ['python-dateutil'] if sys.version_info < (3, 4): @@ -19,7 +22,7 @@ name='dirty-models', url='https://github.com/alfred82santa/dirty-models', author='alfred82santa', - version='0.9.0', + version=dirty_models.__version__, author_email='alfred82santa@gmail.com', license='BSD', classifiers=[ From b2346ce3c4d795735fa0f5a3d049f7cf2ab3f454 Mon Sep 17 00:00:00 2001 From: alfred82santa Date: Sun, 6 Nov 2016 22:27:48 +0100 Subject: [PATCH 07/10] New modifications export --- dirty_models/model_types.py | 44 ++++++-- dirty_models/models.py | 31 +++++ tests/dirty_models/tests_models.py | 176 +++++++++++++++++++++++++---- tests/dirty_models/tests_types.py | 23 +++- 4 files changed, 246 insertions(+), 28 deletions(-) diff --git a/dirty_models/model_types.py b/dirty_models/model_types.py index 7661e50..26d41c9 100644 --- a/dirty_models/model_types.py +++ b/dirty_models/model_types.py @@ -2,9 +2,11 @@ Internal types for dirty models """ import itertools -from .base import BaseData, InnerFieldTypeMixin + from functools import wraps +from .base import BaseData, InnerFieldTypeMixin + def modified_data_decorator(function): """ @@ -19,15 +21,15 @@ def func(self, *args, **kwargs): self.initialise_modified_data() return function(self, *args, **kwargs) return lambda: None + return func class ListModel(InnerFieldTypeMixin, BaseData): - """ Dirty model for a list. It has the behavior to work as a list implementing its methods and has also the methods export_data, export_modified_data, import_data and flat_data - to work also as a model, having the old and the modified values. + to work also as a model, storing original and modified values. """ def __init__(self, seq=None, *args, **kwargs): @@ -241,6 +243,7 @@ def export_modified_data(self): """ Retrieves the modified data in a jsoned form """ + def export_modfield(value, is_modified_seq=True): """ Export modified item @@ -255,10 +258,35 @@ def export_modfield(value, is_modified_seq=True): return [export_modfield(value) for value in self.__modified_data__] return list(x for x in [export_modfield(value) for value in self.__original_data__] if x is not None) + def export_modifications(self): + """ + Returns list modifications. + """ + if self.__modified_data__ is not None: + return self.export_data() + + result = {} + + for key, value in enumerate(self.__original_data__): + try: + if not value.is_modified(): + continue + modifications = value.export_modifications() + except AttributeError: + continue + + try: + result.update({'{}.{}'.format(key, f): v for f, v in modifications.items()}) + except AttributeError: + result[key] = modifications + + return result + def export_original_data(self): """ Retrieves the original_data """ + def export_field(value): """ Export item @@ -267,6 +295,7 @@ def export_field(value): return value.export_original_data() except AttributeError: return value + return [export_field(val) for val in self.__original_data__] def import_data(self, data): @@ -302,12 +331,13 @@ def export_deleted_fields(self): In tree models, deleted fields on children will be appended. """ result = [] - for item in self: + if self.__modified_data__ is not None: + return result + + for index, item in enumerate(self): try: deleted_fields = item.export_deleted_fields() - index = str(self.index(item)) - for key in deleted_fields: - result.append(index + '.' + key) + result.extend(['{}.{}'.format(index, key) for key in deleted_fields]) except AttributeError: pass return result diff --git a/dirty_models/models.py b/dirty_models/models.py index 0a7cb52..b3eff4f 100644 --- a/dirty_models/models.py +++ b/dirty_models/models.py @@ -355,6 +355,37 @@ def export_modified_data(self): return result + def export_modifications(self): + """ + Returns model modifications. + """ + + result = {} + + for key, value in self.__modified_data__.items(): + try: + result[key] = value.export_data() + except AttributeError: + result[key] = value + + for key, value in self.__original_data__.items(): + if key in result.keys() or key in self.__deleted_fields__: + continue + try: + if not value.is_modified(): + continue + modifications = value.export_modifications() + except AttributeError: + continue + + try: + result.update({'{}.{}'.format(key, f): v for f, v in modifications.items()}) + except AttributeError: + result[key] = modifications + + return result + + def get_original_field_value(self, name): """ Returns original field value or None diff --git a/tests/dirty_models/tests_models.py b/tests/dirty_models/tests_models.py index e3e85e6..77734ef 100644 --- a/tests/dirty_models/tests_models.py +++ b/tests/dirty_models/tests_models.py @@ -1,15 +1,15 @@ import pickle from datetime import datetime, date, time, timedelta from enum import Enum +from unittest import TestCase from functools import partial -from unittest import TestCase from dirty_models.base import Unlocker from dirty_models.fields import (BaseField, IntegerField, FloatField, StringField, DateTimeField, ModelField, ArrayField, BooleanField, DateField, TimeField, HashMapField, TimedeltaField, - EnumField) + EnumField, MultiTypeField) from dirty_models.models import BaseModel, DynamicModel, HashMapModel, FastDynamicModel, CamelCaseMeta INITIAL_DATA = { @@ -24,7 +24,6 @@ class PicklableModel(BaseModel): class TestModels(TestCase): - def setUp(self): class FakeModel(BaseModel): testField1 = BaseField() @@ -154,7 +153,7 @@ def test_export_data(self): self.assertEqual(exported_data, {'testField1': 'Value1Modified', 'testField4': {'testField2': - 'Field Value2 Modified', + 'Field Value2 Modified', 'testField1': 'Field Value1'}}) def test_export_modified(self): @@ -753,7 +752,6 @@ class ModelReadOnly(BaseModel): class TestModelReadOnly(TestCase): - def test_no_writing(self): data = { 'testField1': 1, 'testField2': 2, 'testField3': 3, @@ -910,7 +908,6 @@ class TestModel(BaseModel): class TestDynamicModel(TestCase): - def setUp(self): self.model = DynamicModel() self.dict_model = DynamicModel @@ -1065,7 +1062,6 @@ def test_import_none(self): class TestFastDynamicModel(TestDynamicModel): - def setUp(self): self.model = FastDynamicModel() self.dict_model = FastDynamicModel @@ -1082,7 +1078,6 @@ class FastDynamicModelExtraFields(FastDynamicModel): class TestFastDynamicModelExtraFields(TestDynamicModel): - def setUp(self): self.model = FastDynamicModelExtraFields() self.dict_model = FastDynamicModel @@ -1184,7 +1179,6 @@ class PickableHashMapModel(HashMapModel): class TestHashMapModel(TestCase): - def setUp(self): self.model = PickableHashMapModel(field_type=IntegerField()) @@ -1323,7 +1317,6 @@ class ModelDefaultValues(BaseModel): class TestDefaultValues(TestCase): - def test_field_default_value(self): model = ModelDefaultValues() @@ -1388,7 +1381,6 @@ class ModelGeneralDefault(ModelDefaultValues): class TestGeneralDefaultValues(TestCase): - def test_field_default_value(self): model = ModelGeneralDefault() @@ -1442,7 +1434,6 @@ def test_modify_model(self): class CamelCaseMetaclassTests(TestCase): - def test_camelcase_fields(self): class TestModel(BaseModel, metaclass=CamelCaseMeta): test_field_1 = StringField() @@ -1469,7 +1460,6 @@ class TestModel(BaseModel, metaclass=CamelCaseMeta): class AliasTests(TestCase): - def test_field_alias(self): class Model(BaseModel): integer_field = IntegerField(name='scalar_field', alias=['int_field', 'number_field']) @@ -1480,7 +1470,6 @@ class Model(BaseModel): class StructureTests(TestCase): - def test_simple_structure(self): class Model(BaseModel): integer_field = IntegerField(name='scalar_field', alias=['int_field', 'number_field']) @@ -1511,7 +1500,6 @@ class InheritModel(Model): class FieldInconsistenceTests(TestCase): - def test_override_field(self): with self.assertRaises(RuntimeError): class Model(BaseModel): @@ -1520,7 +1508,6 @@ class Model(BaseModel): class GetAttributeByPathTests(TestCase): - def setUp(self): class InnerModel(BaseModel): test_field_1 = IntegerField() @@ -1680,7 +1667,6 @@ def test_list_invalid_path_list(self): class GetAttributeByPathDynamicModelTests(GetAttributeByPathTests): - def setUp(self): self.model = DynamicModel(data={'test_field_1': 1, 'test_list': [{'test_field_1': 2, 'test_field_2': 'string'}, @@ -1690,7 +1676,6 @@ def setUp(self): class GetAttributeByPathFastDynamicModelTests(GetAttributeByPathTests): - def setUp(self): self.model = FastDynamicModel(data={'test_field_1': 1, 'test_list': [{'test_field_1': 2, 'test_field_2': 'string'}, @@ -1700,7 +1685,6 @@ def setUp(self): class AvoidInternalAttributesTests(TestCase): - def test_hashmap_import_double_underscore(self): class Model(HashMapModel): test_field = IntegerField() @@ -1721,7 +1705,6 @@ def test_fastdynmodel_import_double_underscore(self): class ContainsAttributeRegularModelTests(TestCase): - class Model(HashMapModel): test_field = IntegerField() @@ -1763,3 +1746,156 @@ class ContainsAttributeFastDynamicModelTests(ContainsAttributeRegularModelTests) class ContainsAttributeHashMapModelTests(ContainsAttributeRegularModelTests): Model = partial(HashMapModel, field_type=IntegerField()) + + +class ExportModificationsTests(TestCase): + class Model(BaseModel): + test_field_int = IntegerField() + test_array_int = ArrayField(field_type=IntegerField()) + test_array_model = ArrayField(field_type=MultiTypeField(field_types=[IntegerField(), + ModelField()])) + test_array_array_model = ArrayField(field_type=ArrayField(field_type=ModelField())) + test_model = ModelField() + + def test_simple_modified_value(self): + model = self.Model() + model.test_field_int = 3 + + self.assertEqual(model.export_modifications(), {'test_field_int': 3}) + + def test_simple_value(self): + model = self.Model() + model.test_field_int = 3 + + model.flat_data() + + self.assertEqual(model.export_modifications(), {}) + + def test_simple_deleted_value(self): + model = self.Model() + model.test_field_int = 3 + + model.flat_data() + del model.test_field_int + + self.assertEqual(model.export_modifications(), {}) + + def test_inner_model_modified_model(self): + model = self.Model() + model.test_model = {'test_field_int': 3} + + self.assertEqual(model.export_modifications(), {'test_model': {'test_field_int': 3}}) + + def test_inner_model_modified_value(self): + model = self.Model() + model.test_model = {'test_field_int': 3} + model.flat_data() + model.test_model.test_field_int = 4 + + self.assertEqual(model.export_modifications(), {'test_model.test_field_int': 4}) + + def test_inner_model_value(self): + model = self.Model() + model.test_model = {'test_field_int': 3} + model.flat_data() + + self.assertEqual(model.export_modifications(), {}) + + def test_inner_model_deleted_value(self): + model = self.Model() + model.test_model = {'test_field_int': 3} + model.flat_data() + del model.test_model.test_field_int + + self.assertEqual(model.export_modifications(), {}) + + def test_list_int_modified(self): + model = self.Model({'test_array_int': [3, 4]}) + + self.assertEqual(model.export_modifications(), {'test_array_int': [3, 4]}) + + def test_list_int_original(self): + model = self.Model({'test_array_int': [3, 4]}) + model.flat_data() + + self.assertEqual(model.export_modifications(), {}) + + def test_list_int_append_item(self): + model = self.Model({'test_array_int': [3, 4]}) + model.flat_data() + model.test_array_int.append(5) + + self.assertEqual(model.export_modifications(), {'test_array_int': [3, 4, 5]}) + + def test_list_model_modified(self): + model = self.Model({'test_array_model': [{'test_field_int': 3}, + {'test_field_int': 4}]}) + + self.assertEqual(model.export_modifications(), {'test_array_model': [{'test_field_int': 3}, + {'test_field_int': 4}]}) + + def test_list_model_original(self): + model = self.Model({'test_array_model': [{'test_field_int': 3}, + {'test_field_int': 4}]}) + + model.flat_data() + + self.assertEqual(model.export_modifications(), {}) + + def test_list_model_inner_modified(self): + model = self.Model({'test_array_model': [{'test_field_int': 3}, + {'test_field_int': 4}, + 6]}) + + model.flat_data() + model.test_array_model[1].test_field_int = 5 + + self.assertEqual(model.export_modifications(), {'test_array_model.1.test_field_int': 5}) + + def test_list_model_append_item(self): + model = self.Model({'test_array_model': [{'test_field_int': 3}, + {'test_field_int': 4}]}) + + model.flat_data() + model.test_array_model[0].test_field_int = 2 + model.test_array_model[1].test_field_int = 5 + model.test_array_model.append({'test_field_int': 6}) + + self.assertEqual(model.export_modifications(), {'test_array_model': [{'test_field_int': 2}, + {'test_field_int': 5}, + {'test_field_int': 6}]}) + + def test_list_inner_list_model_modified(self): + model = self.Model({'test_array_array_model': [[{'test_field_int': 3}, + {'test_field_int': 4}]]}) + + self.assertEqual(model.export_modifications(), {'test_array_array_model': [[{'test_field_int': 3}, + {'test_field_int': 4}]]}) + + def test_list_inner_list_model_original(self): + model = self.Model({'test_array_array_model': [[{'test_field_int': 3}, + {'test_field_int': 4}]]}) + + model.flat_data() + + self.assertEqual(model.export_modifications(), {}) + + def test_list_inner_list_model_inner_modified(self): + model = self.Model({'test_array_array_model': [[{'test_field_int': 3}, + {'test_field_int': 4}]]}) + + model.flat_data() + model.test_array_array_model[0][1].test_field_int = 5 + + self.assertEqual(model.export_modifications(), {'test_array_array_model.0.1.test_field_int': 5}) + + def test_list_inner_list_model_inner_modified(self): + model = self.Model({'test_array_array_model': [[{'test_field_int': 3}, + {'test_field_int': 4}]]}) + + model.flat_data() + model.test_array_array_model[0].append({'test_field_int': 6}) + + self.assertEqual(model.export_modifications(), {'test_array_array_model.0': [{'test_field_int': 3}, + {'test_field_int': 4}, + {'test_field_int': 6}]}) diff --git a/tests/dirty_models/tests_types.py b/tests/dirty_models/tests_types.py index f44afd5..ec0965a 100644 --- a/tests/dirty_models/tests_types.py +++ b/tests/dirty_models/tests_types.py @@ -1,7 +1,8 @@ from unittest import TestCase from dirty_models.model_types import ListModel -from dirty_models.fields import StringField +from dirty_models.fields import StringField, ArrayField, ModelField, MultiTypeField from dirty_models.fields import IntegerField +from dirty_models.models import BaseModel class TestTypes(TestCase): @@ -222,3 +223,23 @@ def test_not_contains_item_modified_data(self): list_model.flat_data() list_model.pop(0) self.assertFalse(1 in list_model) + + +class ExportDeletedFieldsTests(TestCase): + + class Model(BaseModel): + test_int = IntegerField() + test_array = ArrayField(field_type=MultiTypeField(field_types=[IntegerField(), + ModelField()])) + + def test_inner_model_deleted_field(self): + + model = self.Model({'test_array': [{'test_int': 1}, + {'test_int': 2}, + 3]}) + + model.flat_data() + + del model.test_array[1].test_int + self.assertEqual(model.export_deleted_fields(), ['test_array.1.test_int']) + From 6c5ca209fc2f51a8a76a6f1c554b101d114fc2c4 Mon Sep 17 00:00:00 2001 From: alfred82santa Date: Sun, 6 Nov 2016 22:28:52 +0100 Subject: [PATCH 08/10] PEP8 --- dirty_models/__init__.py | 2 +- dirty_models/fields.py | 7 ++++--- dirty_models/models.py | 1 - dirty_models/utils.py | 2 ++ tests/dirty_models/tests_models.py | 20 +++++++++++++++++++- tests/dirty_models/tests_types.py | 1 - tests/dirty_models/tests_utils.py | 6 +++++- 7 files changed, 31 insertions(+), 8 deletions(-) diff --git a/dirty_models/__init__.py b/dirty_models/__init__.py index 3dec7f4..8a6dd4c 100644 --- a/dirty_models/__init__.py +++ b/dirty_models/__init__.py @@ -7,4 +7,4 @@ from .models import * from .fields import * -__version__ = '0.9.0' \ No newline at end of file +__version__ = '0.9.0' diff --git a/dirty_models/fields.py b/dirty_models/fields.py index 5766efe..d36baa6 100644 --- a/dirty_models/fields.py +++ b/dirty_models/fields.py @@ -169,7 +169,7 @@ def check_value(self, value): @can_use_enum def can_use_value(self, value): return isinstance(value, float) \ - or (isinstance(value, str) and value.isdigit()) + or (isinstance(value, str) and value.isdigit()) class FloatField(BaseField): @@ -195,8 +195,8 @@ def check_value(self, value): @can_use_enum def can_use_value(self, value): return isinstance(value, int) \ - or (isinstance(value, str) and - value.replace('.', '', 1).isnumeric()) + or (isinstance(value, str) and + value.replace('.', '', 1).isnumeric()) class BooleanField(BaseField): @@ -681,6 +681,7 @@ def __set__(self, obj, value): class InnerFieldTypeMixin: + def __init__(self, field_type=None, **kwargs): self._field_type = None if isinstance(field_type, tuple): diff --git a/dirty_models/models.py b/dirty_models/models.py index b3eff4f..05b13f9 100644 --- a/dirty_models/models.py +++ b/dirty_models/models.py @@ -385,7 +385,6 @@ def export_modifications(self): return result - def get_original_field_value(self, name): """ Returns original field value or None diff --git a/dirty_models/utils.py b/dirty_models/utils.py index 789fe62..056c24c 100644 --- a/dirty_models/utils.py +++ b/dirty_models/utils.py @@ -21,6 +21,7 @@ class BaseFormatterIter: class BaseFieldtypeFormatterIter(BaseFormatterIter): + def __init__(self, obj, field, parent_formatter): self.obj = obj self.field = field @@ -28,6 +29,7 @@ def __init__(self, obj, field, parent_formatter): class ListFormatterIter(BaseFieldtypeFormatterIter): + def __iter__(self): for item in self.obj: yield self.parent_formatter.format_field(self.field, item) diff --git a/tests/dirty_models/tests_models.py b/tests/dirty_models/tests_models.py index 77734ef..c6d1ac5 100644 --- a/tests/dirty_models/tests_models.py +++ b/tests/dirty_models/tests_models.py @@ -24,6 +24,7 @@ class PicklableModel(BaseModel): class TestModels(TestCase): + def setUp(self): class FakeModel(BaseModel): testField1 = BaseField() @@ -153,7 +154,7 @@ def test_export_data(self): self.assertEqual(exported_data, {'testField1': 'Value1Modified', 'testField4': {'testField2': - 'Field Value2 Modified', + 'Field Value2 Modified', 'testField1': 'Field Value1'}}) def test_export_modified(self): @@ -752,6 +753,7 @@ class ModelReadOnly(BaseModel): class TestModelReadOnly(TestCase): + def test_no_writing(self): data = { 'testField1': 1, 'testField2': 2, 'testField3': 3, @@ -908,6 +910,7 @@ class TestModel(BaseModel): class TestDynamicModel(TestCase): + def setUp(self): self.model = DynamicModel() self.dict_model = DynamicModel @@ -1062,6 +1065,7 @@ def test_import_none(self): class TestFastDynamicModel(TestDynamicModel): + def setUp(self): self.model = FastDynamicModel() self.dict_model = FastDynamicModel @@ -1078,6 +1082,7 @@ class FastDynamicModelExtraFields(FastDynamicModel): class TestFastDynamicModelExtraFields(TestDynamicModel): + def setUp(self): self.model = FastDynamicModelExtraFields() self.dict_model = FastDynamicModel @@ -1179,6 +1184,7 @@ class PickableHashMapModel(HashMapModel): class TestHashMapModel(TestCase): + def setUp(self): self.model = PickableHashMapModel(field_type=IntegerField()) @@ -1317,6 +1323,7 @@ class ModelDefaultValues(BaseModel): class TestDefaultValues(TestCase): + def test_field_default_value(self): model = ModelDefaultValues() @@ -1381,6 +1388,7 @@ class ModelGeneralDefault(ModelDefaultValues): class TestGeneralDefaultValues(TestCase): + def test_field_default_value(self): model = ModelGeneralDefault() @@ -1434,6 +1442,7 @@ def test_modify_model(self): class CamelCaseMetaclassTests(TestCase): + def test_camelcase_fields(self): class TestModel(BaseModel, metaclass=CamelCaseMeta): test_field_1 = StringField() @@ -1460,6 +1469,7 @@ class TestModel(BaseModel, metaclass=CamelCaseMeta): class AliasTests(TestCase): + def test_field_alias(self): class Model(BaseModel): integer_field = IntegerField(name='scalar_field', alias=['int_field', 'number_field']) @@ -1470,6 +1480,7 @@ class Model(BaseModel): class StructureTests(TestCase): + def test_simple_structure(self): class Model(BaseModel): integer_field = IntegerField(name='scalar_field', alias=['int_field', 'number_field']) @@ -1500,6 +1511,7 @@ class InheritModel(Model): class FieldInconsistenceTests(TestCase): + def test_override_field(self): with self.assertRaises(RuntimeError): class Model(BaseModel): @@ -1508,6 +1520,7 @@ class Model(BaseModel): class GetAttributeByPathTests(TestCase): + def setUp(self): class InnerModel(BaseModel): test_field_1 = IntegerField() @@ -1667,6 +1680,7 @@ def test_list_invalid_path_list(self): class GetAttributeByPathDynamicModelTests(GetAttributeByPathTests): + def setUp(self): self.model = DynamicModel(data={'test_field_1': 1, 'test_list': [{'test_field_1': 2, 'test_field_2': 'string'}, @@ -1676,6 +1690,7 @@ def setUp(self): class GetAttributeByPathFastDynamicModelTests(GetAttributeByPathTests): + def setUp(self): self.model = FastDynamicModel(data={'test_field_1': 1, 'test_list': [{'test_field_1': 2, 'test_field_2': 'string'}, @@ -1685,6 +1700,7 @@ def setUp(self): class AvoidInternalAttributesTests(TestCase): + def test_hashmap_import_double_underscore(self): class Model(HashMapModel): test_field = IntegerField() @@ -1705,6 +1721,7 @@ def test_fastdynmodel_import_double_underscore(self): class ContainsAttributeRegularModelTests(TestCase): + class Model(HashMapModel): test_field = IntegerField() @@ -1749,6 +1766,7 @@ class ContainsAttributeHashMapModelTests(ContainsAttributeRegularModelTests): class ExportModificationsTests(TestCase): + class Model(BaseModel): test_field_int = IntegerField() test_array_int = ArrayField(field_type=IntegerField()) diff --git a/tests/dirty_models/tests_types.py b/tests/dirty_models/tests_types.py index ec0965a..b9c5f48 100644 --- a/tests/dirty_models/tests_types.py +++ b/tests/dirty_models/tests_types.py @@ -242,4 +242,3 @@ def test_inner_model_deleted_field(self): del model.test_array[1].test_int self.assertEqual(model.export_deleted_fields(), ['test_array.1.test_int']) - diff --git a/tests/dirty_models/tests_utils.py b/tests/dirty_models/tests_utils.py index 467525b..5da7d57 100644 --- a/tests/dirty_models/tests_utils.py +++ b/tests/dirty_models/tests_utils.py @@ -10,6 +10,7 @@ class UnderscoreToCamelTests(TestCase): + def test_no_underscore(self): self.assertEqual(underscore_to_camel('foobar'), 'foobar') @@ -27,6 +28,7 @@ def test_underscore_multi_number(self): class TestModel(BaseModel): + class TestEnum(Enum): value_1 = 1 value_2 = '2' @@ -39,7 +41,7 @@ class TestEnum(Enum): test_array_multitype = ArrayField(field_type=MultiTypeField(field_types=[IntegerField(), DateTimeField( parse_format="%Y-%m-%dT%H:%M:%S" - )])) + )])) test_model_field_1 = ArrayField(field_type=ArrayField(field_type=ModelField())) test_hash_map = HashMapField(field_type=DateField(parse_format="%Y-%m-%d date")) test_timedelta = TimedeltaField() @@ -49,6 +51,7 @@ class TestEnum(Enum): class ModelFormatterIterTests(TestCase): + def test_model_formatter(self): model = TestModel(data={'test_string_field_1': 'foo', 'test_int_field_1': 4, @@ -152,6 +155,7 @@ def test_fast_dynamic_model_formatter(self): class JSONEncoderTests(TestCase): + def test_model_json(self): model = TestModel(data={'test_string_field_1': 'foo', 'test_int_field_1': 4, From 3b9447b45626ccf5a02d72bc40770a5b039b7d25 Mon Sep 17 00:00:00 2001 From: alfred82santa Date: Sun, 6 Nov 2016 22:29:57 +0100 Subject: [PATCH 09/10] Fix tests --- tests/dirty_models/tests_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dirty_models/tests_models.py b/tests/dirty_models/tests_models.py index c6d1ac5..dcae2a6 100644 --- a/tests/dirty_models/tests_models.py +++ b/tests/dirty_models/tests_models.py @@ -1907,7 +1907,7 @@ def test_list_inner_list_model_inner_modified(self): self.assertEqual(model.export_modifications(), {'test_array_array_model.0.1.test_field_int': 5}) - def test_list_inner_list_model_inner_modified(self): + def test_list_inner_list_model_append_item(self): model = self.Model({'test_array_array_model': [[{'test_field_int': 3}, {'test_field_int': 4}]]}) From 598b875a9d09d2a49390aacdfd22f4f5aea7b00a Mon Sep 17 00:00:00 2001 From: alfred82santa Date: Sun, 6 Nov 2016 22:48:56 +0100 Subject: [PATCH 10/10] Add some documentations --- README.rst | 1 + docs/source/getting_started.rst | 45 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/README.rst b/README.rst index 923e36c..472ce84 100644 --- a/README.rst +++ b/README.rst @@ -95,6 +95,7 @@ Version 0.9.0 - Fixes on code. - Added ``__version__`` to main package file. - Synchronized version between main packege file, ``setup.py`` and docs. +- Export only modifications. Version 0.8.1 diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index c6804e5..7b103f6 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -212,3 +212,48 @@ Some time you want to set data to whole model, but model already exists, so you assert model.my_int_field == 3 # True assert model.my_string_field == 'string' # True + + +--------------- +How to get data +--------------- + +In the same way, there are several methods to get data from model. + +Use data from field +=================== + +It is the simplest way to get data. Just use field. + +.. code-block:: python + + class MyModel(BaseModel): + + my_int_field = IntegerField() + my_string_field = StringField() + + model = MyModel() + + model.my_int_field = 3 + + assert model.my_int_field == 3 # True + + +Export data +=========== + +It is possible to export data to a dict. + +.. code-block:: python + + class MyModel(BaseModel): + + my_int_field = IntegerField() + my_string_field = StringField() + + model = MyModel() + + model.my_int_field = 3 + + print(model.export_data()) + # {'my_int_field': 3} \ No newline at end of file