diff --git a/README.rst b/README.rst index 00f6ea7..7512526 100644 --- a/README.rst +++ b/README.rst @@ -85,6 +85,15 @@ Features Changelog --------- +Version 0.8.1 +------------- + +- Added __contains__ function to models and lists. It allows to use ``in`` operator. +- Added ``default_timezone`` parameter to DateTimeFields and TimeFields. If value entered has no a timezone + defined, default one will be set. +- Added ``force_timezone`` parameter to DateTimeFields in order to convert values to a specific timezone. +- More cleanups. + Version 0.8.0 ------------- diff --git a/dirty_models/fields.py b/dirty_models/fields.py index 5297f3e..3f33505 100644 --- a/dirty_models/fields.py +++ b/dirty_models/fields.py @@ -3,10 +3,11 @@ """ from datetime import datetime, date, time, timedelta -from dateutil.parser import parse as dateutil_parse -from .model_types import ListModel + from collections import Mapping +from dateutil.parser import parse as dateutil_parse +from .model_types import ListModel __all__ = ['IntegerField', 'FloatField', 'BooleanField', 'StringField', 'StringIdField', 'TimeField', 'DateField', 'DateTimeField', 'TimedeltaField', 'ModelField', 'ArrayField', @@ -14,7 +15,6 @@ class BaseField: - """Base field descriptor.""" def __init__(self, name=None, alias=None, getter=None, setter=None, read_only=False, default=None, doc=None): @@ -105,7 +105,6 @@ def __delete__(self, obj): class IntegerField(BaseField): - """ It allows to use an integer as value in a field. @@ -125,11 +124,10 @@ 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): - """ It allows to use a float as value in a field. @@ -148,12 +146,11 @@ 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()) class BooleanField(BaseField): - """ It allows to use a boolean as value in a field. @@ -181,7 +178,6 @@ def can_use_value(self, value): class StringField(BaseField): - """ It allows to use a string as value in a field. @@ -204,7 +200,6 @@ def can_use_value(self, value): class StringIdField(StringField): - """ It allows to use a string as value in a field, but not allows empty strings. Empty string are like ``None`` and they will remove data of field. @@ -225,7 +220,6 @@ def set_value(self, obj, value): class DateTimeBaseField(BaseField): - """Base field for time or/and date fields.""" date_parsers = {} @@ -242,7 +236,6 @@ def __init__(self, parse_format=None, **kwargs): :type parse_format: str or dict """ super(DateTimeBaseField, self).__init__(**kwargs) - self._parse_format = None self.parse_format = parse_format def export_definition(self): @@ -250,16 +243,6 @@ def export_definition(self): result['parse_format'] = self.parse_format return result - @property - def parse_format(self): - """Parse_format getter: datetime format used on field""" - return self._parse_format - - @parse_format.setter - def parse_format(self, value): - """Parse_format setter: datetime format used on field""" - self._parse_format = value - def get_parsed_value(self, value): """ Helper to cast string to datetime using :member:`parse_format`. @@ -300,6 +283,7 @@ def get_formatted_value(self, value): :type value: datetime :return: str """ + def get_formatter(parser_desc): try: return parser_desc['formatter'] @@ -330,7 +314,6 @@ def get_formatter(parser_desc): class TimeField(DateTimeBaseField): - """ It allows to use a time as value in a field. @@ -347,6 +330,24 @@ class TimeField(DateTimeBaseField): * :class:`~datetime.datetime` will get time part. """ + def __init__(self, parse_format=None, default_timezone=None, **kwargs): + """ + + :param parse_format: String format to cast string to datetime. It could be + an string format or a :class:`dict` with two keys: + + * ``parser`` key to set how string must be parsed. It could be a callable. + * ``formatter`` key to set how datetime must be formatted. It could be a callable. + + :type parse_format: str or dict + + :param default_timezone: Default timezone to use when value does not have one. + + :type default_timezone: datetime.tzinfo + """ + super(TimeField, self).__init__(parse_format=parse_format, **kwargs) + self.default_timezone = default_timezone + def convert_value(self, value): if isinstance(value, list): return time(*value) @@ -372,9 +373,21 @@ def check_value(self, value): def can_use_value(self, value): return isinstance(value, (int, str, datetime, list, dict)) + def set_value(self, obj, value: time): + if self.default_timezone and value.tzinfo is None: + value = time(hour=value.hour, minute=value.minute, second=value.microsecond, + microsecond=value.microsecond, tzinfo=self.default_timezone) -class DateField(DateTimeBaseField): + super(TimeField, self).set_value(obj, value) + + def export_definition(self): + result = super(TimeField, self).export_definition() + if self.default_timezone: + result['default_timezone'] = self.default_timezone + return result + +class DateField(DateTimeBaseField): """ It allows to use a date as value in a field. @@ -418,7 +431,6 @@ def can_use_value(self, value): class DateTimeField(DateTimeBaseField): - """ It allows to use a datetime as value in a field. @@ -435,6 +447,30 @@ class DateTimeField(DateTimeBaseField): * :class:`~datetime.date` will set date part. """ + def __init__(self, parse_format=None, default_timezone=None, force_timezone=False, **kwargs): + """ + + :param parse_format: String format to cast string to datetime. It could be + an string format or a :class:`dict` with two keys: + + * ``parser`` key to set how string must be parsed. It could be a callable. + * ``formatter`` key to set how datetime must be formatted. It could be a callable. + + :type parse_format: str or dict + + :param default_timezone: Default timezone to use when value does not have one. + + :type default_timezone: datetime.tzinfo + + :param force_timezone: If it is True value will be converted to timezone defined on ``default_timezone`` + parameter. It ``default_timezone`` is not defined it is ignored. + + :type: bool + """ + super(DateTimeField, self).__init__(parse_format=parse_format, **kwargs) + self.default_timezone = default_timezone + self.force_timezone = force_timezone + def convert_value(self, value): if isinstance(value, list): return datetime(*value) @@ -460,6 +496,22 @@ def check_value(self, value): def can_use_value(self, value): return isinstance(value, (int, str, date, dict, list)) + def set_value(self, obj, value): + if self.default_timezone: + if value.tzinfo is None: + value = value.replace(tzinfo=self.default_timezone) + elif self.force_timezone and value.tzinfo != self.default_timezone: + value = value.astimezone(tz=self.default_timezone) + + super(DateTimeField, self).set_value(obj, value) + + def export_definition(self): + result = super(DateTimeField, self).export_definition() + if self.default_timezone: + result['default_timezone'] = self.default_timezone + result['force_timezone'] = self.force_timezone + return result + class TimedeltaField(BaseField): """ @@ -485,7 +537,6 @@ def can_use_value(self, value): class ModelField(BaseField): - """ It allows to use a model as value in a field. Model type must be defined on constructor using param model_class. If it is not defined @@ -505,7 +556,7 @@ def __init__(self, model_class=None, **kwargs): self._model_setter = None if 'setter' in kwargs: self._model_setter = kwargs['setter'] - del(kwargs['setter']) + del (kwargs['setter']) super(ModelField, self).__init__(**kwargs) def export_definition(self): @@ -556,7 +607,6 @@ def __set__(self, obj, value): class InnerFieldTypeMixin: - def __init__(self, field_type=None, **kwargs): self._field_type = None if isinstance(field_type, tuple): @@ -581,7 +631,6 @@ def field_type(self, value): class ArrayField(InnerFieldTypeMixin, BaseField): - """ It allows to create a ListModel (iterable in :mod:`dirty_models.types`) of different elements according to the specified field_type. So it is possible to have a list of Integers, Strings, Models, etc. @@ -652,7 +701,6 @@ def autolist(self, value): class HashMapField(InnerFieldTypeMixin, ModelField): - """ It allows to create a field which contains a hash map. @@ -675,7 +723,6 @@ def convert_value(self, value): class BlobField(BaseField): - """ It allows any type of data. """ @@ -683,7 +730,6 @@ class BlobField(BaseField): class MultiTypeField(BaseField): - """ It allows to define multiple type for a field. So, it is possible to define a field as a integer and as a model field, for example. @@ -703,7 +749,7 @@ def __init__(self, field_types=None, **kwargs): def get_field_docstring(self): if len(self._field_types): return 'Multiple type values are allowed:\n\n{0}'.format( - "\n\n".join(["* {0}".format(field.get_field_docstring())for field in self._field_types])) + "\n\n".join(["* {0}".format(field.get_field_docstring()) for field in self._field_types])) def export_definition(self): result = super(MultiTypeField, self).export_definition() diff --git a/dirty_models/model_types.py b/dirty_models/model_types.py index 3815140..7661e50 100644 --- a/dirty_models/model_types.py +++ b/dirty_models/model_types.py @@ -30,9 +30,6 @@ class ListModel(InnerFieldTypeMixin, BaseData): to work also as a model, having the old and the modified values. """ - __original_data__ = None - __modified_data__ = None - def __init__(self, seq=None, *args, **kwargs): super(ListModel, self).__init__(*args, **kwargs) self.__original_data__ = [] @@ -465,3 +462,6 @@ def __repr__(self): def __str__(self): return str([item for item in self]) + + def __contains__(self, item): + return item in self.__modified_data__ if self.__modified_data__ is not None else item in self.__original_data__ diff --git a/dirty_models/models.py b/dirty_models/models.py index 606bf9b..f254bcc 100644 --- a/dirty_models/models.py +++ b/dirty_models/models.py @@ -156,17 +156,14 @@ class BaseModel(BaseData, metaclass=DirtyModelMeta): Base model with dirty feature. It stores original data and saves modifications in other side. """ - __original_data__ = None - __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__ = {} - self.__modified_data__ = {} - self.__deleted_fields__ = [] + BaseModel.__setattr__(self, '__original_data__', {}) + BaseModel.__setattr__(self, '__modified_data__', {}) + BaseModel.__setattr__(self, '__deleted_fields__', []) self.unlock() self.import_data(self.__default_data__) @@ -511,6 +508,10 @@ def __str__(self): def __repr__(self): return str(self) + def __contains__(self, item): + return item in self.__modified_data__ or (item in self.__original_data__ and + item not in self.__deleted_fields__) + @classmethod def get_field_obj(cls, name): obj_field = getattr(cls, name, None) diff --git a/setup.py b/setup.py index 60fa957..8ccb5f5 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ name='dirty-models', url='https://github.com/alfred82santa/dirty-models', author='alfred82santa', - version='0.8.0', + version='0.8.1', author_email='alfred82santa@gmail.com', classifiers=[ 'Intended Audience :: Developers', @@ -16,7 +16,7 @@ 'License :: OSI Approved :: BSD License', 'Development Status :: 4 - Beta'], packages=['dirty_models'], - include_package_data=True, + 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(), diff --git a/tests/dirty_models/tests_fields.py b/tests/dirty_models/tests_fields.py index b574f6c..d6ecdef 100644 --- a/tests/dirty_models/tests_fields.py +++ b/tests/dirty_models/tests_fields.py @@ -1,12 +1,14 @@ +from datetime import time, date, datetime, timezone, timedelta from unittest import TestCase + +import iso8601 +from dateutil import tz + from dirty_models.fields import (IntegerField, StringField, BooleanField, FloatField, ModelField, TimeField, DateField, DateTimeField, ArrayField, StringIdField, HashMapField, MultiTypeField, TimedeltaField) -from dirty_models.models import BaseModel, HashMapModel from dirty_models.model_types import ListModel - -from datetime import time, date, datetime, timezone, timedelta -import iso8601 +from dirty_models.models import BaseModel, HashMapModel class TestFields(TestCase): @@ -135,7 +137,6 @@ def test_bool_field_using_bool_false(self): self.assertFalse(field.use_value(False)) def test_int_field_on_class_using_int(self): - class TestModel(BaseModel): field_name = IntegerField() @@ -144,7 +145,6 @@ class TestModel(BaseModel): self.assertEqual(model.field_name, 3) def test_string_field_on_class_using_empty_string(self): - class TestModel(BaseModel): field_name = StringField() @@ -153,7 +153,6 @@ class TestModel(BaseModel): self.assertEqual(model.field_name, "") def test_string_id_field_on_class_using_string(self): - class TestModel(BaseModel): field_name = StringIdField() @@ -162,7 +161,6 @@ class TestModel(BaseModel): self.assertIsNotNone(model.field_name) def test_string_id_field_on_class_using_number(self): - class TestModel(BaseModel): field_name = StringIdField() @@ -171,7 +169,6 @@ class TestModel(BaseModel): self.assertIsNotNone(model.field_name) def test_string_id_field_on_class_using_empty_string(self): - class TestModel(BaseModel): field_name = StringIdField() @@ -180,7 +177,6 @@ class TestModel(BaseModel): self.assertIsNone(model.field_name) def test_string_id_field_on_class_using_empty_string_and_delete_value(self): - class TestModel(BaseModel): field_name = StringIdField() @@ -190,7 +186,6 @@ class TestModel(BaseModel): self.assertIsNone(model.field_name) def test_int_field_on_class_using_float(self): - class TestModel(BaseModel): field_name = IntegerField() @@ -199,7 +194,6 @@ class TestModel(BaseModel): self.assertEqual(model.field_name, 3) def test_int_field_on_class_using_str(self): - class TestModel(BaseModel): field_name = IntegerField() @@ -208,7 +202,6 @@ class TestModel(BaseModel): self.assertEqual(model.field_name, 3) def test_int_field_on_class_using_dict(self): - class TestModel(BaseModel): field_name = IntegerField() @@ -217,7 +210,6 @@ class TestModel(BaseModel): self.assertIsNone(model.field_name) def test_float_field_on_class_using_int(self): - class TestModel(BaseModel): field_name = FloatField() @@ -226,7 +218,6 @@ class TestModel(BaseModel): self.assertEqual(model.field_name, 3.0) def test_float_field_on_class_using_float(self): - class TestModel(BaseModel): field_name = FloatField() @@ -235,7 +226,6 @@ class TestModel(BaseModel): self.assertEqual(model.field_name, 3.0) def test_float_field_on_class_using_str(self): - class TestModel(BaseModel): field_name = FloatField() @@ -244,7 +234,6 @@ class TestModel(BaseModel): self.assertEqual(model.field_name, 3.0) def test_float_field_on_class_using_dict(self): - class TestModel(BaseModel): field_name = FloatField() @@ -253,7 +242,6 @@ class TestModel(BaseModel): self.assertIsNone(model.field_name) def test_str_field_on_class_using_int(self): - class TestModel(BaseModel): field_name = StringField() @@ -262,7 +250,6 @@ class TestModel(BaseModel): self.assertEqual(model.field_name, "3") def test_str_field_on_class_using_float(self): - class TestModel(BaseModel): field_name = StringField() @@ -271,7 +258,6 @@ class TestModel(BaseModel): self.assertEqual(model.field_name, "3.0") def test_str_field_on_class_using_str(self): - class TestModel(BaseModel): field_name = StringField() @@ -280,7 +266,6 @@ class TestModel(BaseModel): self.assertEqual(model.field_name, "aaaaa") def test_str_field_on_class_using_dict(self): - class TestModel(BaseModel): field_name = StringField() @@ -289,7 +274,6 @@ class TestModel(BaseModel): self.assertIsNone(model.field_name) def test_bool_field_on_class_using_int_true(self): - class TestModel(BaseModel): field_name = BooleanField() @@ -298,7 +282,6 @@ class TestModel(BaseModel): self.assertTrue(model.field_name) def test_bool_field_on_class_using_int_false(self): - class TestModel(BaseModel): field_name = BooleanField() @@ -307,7 +290,6 @@ class TestModel(BaseModel): self.assertFalse(model.field_name) def test_bool_field_on_class_using_float(self): - class TestModel(BaseModel): field_name = BooleanField() @@ -316,7 +298,6 @@ class TestModel(BaseModel): self.assertIsNone(model.field_name) def test_bool_field_on_class_using_str_any(self): - class TestModel(BaseModel): field_name = BooleanField() @@ -325,7 +306,6 @@ class TestModel(BaseModel): self.assertFalse(model.field_name) def test_bool_field_on_class_using_str_false(self): - class TestModel(BaseModel): field_name = BooleanField() @@ -334,7 +314,6 @@ class TestModel(BaseModel): self.assertFalse(model.field_name) def test_bool_field_on_class_using_str_true(self): - class TestModel(BaseModel): field_name = BooleanField() @@ -343,7 +322,6 @@ class TestModel(BaseModel): self.assertTrue(model.field_name) def test_bool_field_on_class_using_dict(self): - class TestModel(BaseModel): field_name = BooleanField() @@ -352,7 +330,6 @@ class TestModel(BaseModel): self.assertIsNone(model.field_name) def test_int_field_delete_value(self): - class TestModel(BaseModel): field_name = IntegerField() @@ -368,7 +345,6 @@ class TestModel(BaseModel): self.assertEqual(model.field_name, 3) def test_int_field_bad_definition(self): - class TestModel(): field_name = IntegerField() @@ -384,7 +360,6 @@ class TestModel(): del model.field_name def test_model_field_on_class_using_int(self): - class TestModel(BaseModel): field_name = ModelField() @@ -393,7 +368,6 @@ class TestModel(BaseModel): self.assertIsNone(model.field_name) def test_model_field_on_class_using_float(self): - class TestModel(BaseModel): field_name = ModelField() @@ -402,7 +376,6 @@ class TestModel(BaseModel): self.assertIsNone(model.field_name) def test_model_field_on_class_using_str(self): - class TestModel(BaseModel): field_name = ModelField() @@ -411,7 +384,6 @@ class TestModel(BaseModel): self.assertIsNone(model.field_name) def test_model_field_on_class_using_dict(self): - class TestModel(BaseModel): field_name_1 = ModelField() field_name_2 = StringField() @@ -422,7 +394,6 @@ class TestModel(BaseModel): self.assertEqual(model.field_name_1.field_name_2, "ooo") def test_model_field_import_parent(self): - class TestModel(BaseModel): field_name_1 = ModelField() field_name_2 = StringField() @@ -435,7 +406,6 @@ class TestModel(BaseModel): self.assertEqual(model.field_name_1.field_name_2, "eee") def test_model_field_on_class_using_model_with_original_data(self): - class TestModel(BaseModel): field_name_1 = ModelField() field_name_2 = StringField() @@ -450,7 +420,6 @@ class TestModel(BaseModel): self.assertEqual(model.field_name_1.field_name_2, "aaa") def test_model_field_on_class_using_dict_with_original_data(self): - class TestModel(BaseModel): field_name_1 = ModelField() field_name_2 = StringField() @@ -465,7 +434,6 @@ class TestModel(BaseModel): self.assertEqual(model.field_name_1.field_name_2, "aaa") def test_model_field_bad_definition(self): - class TestModel(): field_name = ModelField() @@ -482,7 +450,7 @@ def test_time_field_using_int(self): datetime(year=1970, month=1, day=1, hour=0, minute=55, second=33, tzinfo=timezone.utc).astimezone() - .time()) + .time()) def test_time_field_desc(self): field = TimeField() @@ -561,7 +529,7 @@ def test_date_field_using_int(self): datetime(year=2015, month=6, day=3, hour=0, minute=15, second=33, tzinfo=timezone.utc).astimezone() - .date()) + .date()) def test_date_field_using_float(self): field = DateField() @@ -683,7 +651,7 @@ def test_datetime_field_using_datetime(self): hour=0, minute=15, second=33, tzinfo=timezone.utc)) - def test_datetime_field_using_is8061_parser_and_formatter(self): + def test_datetime_field_using_iso8061_parser_and_formatter(self): field = DateTimeField('iso8061') field.date_parsers = { 'iso8061': { @@ -698,7 +666,7 @@ def test_datetime_field_using_is8061_parser_and_formatter(self): hour=13, minute=2, second=41, tzinfo=timezone.utc)) - def test_datetime_field_using_is8061_without_formatter(self): + def test_datetime_field_using_iso8061_without_formatter(self): field = DateTimeField('iso8061') field.date_parsers = { 'iso8061': { @@ -710,7 +678,7 @@ def test_datetime_field_using_is8061_without_formatter(self): self.assertTrue(field.can_use_value(data)) self.assertIsNone(field.use_value(data)) - def test_datetime_field_using_is8061_without_parser(self): + def test_datetime_field_using_iso8061_without_parser(self): field = DateTimeField('iso8061') field.date_parsers = { 'iso8061': { @@ -722,10 +690,10 @@ def test_datetime_field_using_is8061_without_parser(self): self.assertTrue(field.can_use_value(data)) self.assertIsNotNone(field.use_value(data)) - def test_datetime_field_using_is8061_parser_and_def_formatter(self): - + def test_datetime_field_using_iso8061_parser_and_def_formatter(self): def parser_format(value): return datetime.strptime(datetime.strftime(value, '%Y-%m-%dT%H:%M:%SZ'), '%Y-%m-%dT%H:%M:%SZ') + field = DateTimeField('iso8061') field.date_parsers = { 'iso8061': { @@ -740,7 +708,7 @@ def parser_format(value): hour=13, minute=2, second=41, tzinfo=timezone.utc)) - def test_datetime_field_using_is8061_bad_str(self): + def test_datetime_field_using_iso8061_bad_str(self): field = DateTimeField('iso8061') field.date_parsers = { 'iso8061': { @@ -754,7 +722,7 @@ def test_datetime_field_using_is8061_bad_str(self): self.assertTrue(field.can_use_value(data)) self.assertIsNone(field.use_value(data)) - def test_time_field_using_is8061(self): + def test_time_field_using_iso8061(self): field = TimeField('iso8061') field.date_parsers = { 'iso8061': { @@ -769,7 +737,7 @@ def test_time_field_using_is8061(self): self.assertEqual(field.use_value(data), time(hour=13, minute=2, second=41, tzinfo=timezone.utc)) - def test_time_field_using_is8061_bad_str(self): + def test_time_field_using_iso8061_bad_str(self): field = TimeField('iso8061') field.date_parsers = { 'iso8061': { @@ -783,7 +751,7 @@ def test_time_field_using_is8061_bad_str(self): self.assertTrue(field.can_use_value(data)) self.assertIsNone(field.use_value(data)) - def test_date_field_using_is8061(self): + def test_date_field_using_iso8061(self): field = DateField('iso8061') field.date_parsers = { 'iso8061': { @@ -811,8 +779,7 @@ def test_date_field_using_is8061_bad_str(self): self.assertTrue(field.can_use_value(data)) self.assertIsNone(field.use_value(data)) - def test_datetime_field_using_is8061_def_format(self): - + def test_datetime_field_using_iso8061_def_format(self): def get_format(value): format = '%Y-%m-%dT%H:%M:%SZ' return datetime.strftime(value, format) @@ -830,8 +797,7 @@ def get_format(value): tzinfo=timezone.utc) self.assertEqual(field.get_formatted_value(data), '2012-09-11T13:02:41Z') - def test_date_field_using_is8061_bad_format_str(self): - + def test_date_field_using_iso8061_bad_format_str(self): field = DateTimeField() data = datetime(year=2012, month=9, day=11, @@ -839,8 +805,7 @@ def test_date_field_using_is8061_bad_format_str(self): tzinfo=timezone.utc) self.assertEqual(field.get_formatted_value(data), '2012-09-11 13:02:41+00:00') - def test_date_field_using_is8061_format_str(self): - + def test_date_field_using_iso8061_format_str(self): field = DateTimeField('iso8061') field.date_parsers = { 'iso8061': { @@ -900,6 +865,7 @@ def test_datetime_field_using_default_parser_formatter(self): def test_model_field_desc(self): class TestModel(BaseModel): field_name = StringIdField() + field = ModelField(model_class=TestModel) self.assertEqual(field.export_definition(), {'alias': None, @@ -909,7 +875,6 @@ class TestModel(BaseModel): 'read_only': False}) def test_array_field(self): - class TestModel(BaseModel): field_name_1 = ModelField() field_name_2 = StringField() @@ -928,7 +893,6 @@ class ArrayModel(BaseModel): self.assertEqual(test_model_1, array_model.array_field[0]) def test_array_field_with_ListModel(self): - class TestModel(BaseModel): field_name_1 = ModelField() field_name_2 = StringField() @@ -946,7 +910,6 @@ class ArrayModel(BaseModel): self.assertEqual(test_model_1, array_model.array_field[0]) def test_array_field_extend(self): - class TestModel(BaseModel): field_name_1 = ModelField() field_name_2 = StringField() @@ -966,7 +929,6 @@ class ArrayModel(BaseModel): self.assertEqual(test_model_2, array_model.array_field[1]) def test_array_field_invalid_value_to_add(self): - class TestModel(BaseModel): field_name_1 = ModelField() field_name_2 = StringField() @@ -985,7 +947,6 @@ class ArrayModel(BaseModel): self.assertRaises(IndexError, array_model.array_field.__getitem__, 1) def test_array_field_invalid_value_set(self): - class TestModel(BaseModel): field_name_1 = ModelField() field_name_2 = StringField() @@ -999,7 +960,6 @@ class ArrayModel(BaseModel): self.assertEqual(1, len(array_model.array_field)) def test_array_field_not_iterable(self): - class ArrayModel(BaseModel): array_field = ArrayField(field_type=BooleanField()) @@ -1016,7 +976,6 @@ class ArrayModel(BaseModel): self.assertIsNone(model.array_field) def test_array_field_conversion(self): - class ArrayModel(BaseModel): array_field = ArrayField(field_type=IntegerField()) @@ -1025,7 +984,6 @@ class ArrayModel(BaseModel): self.assertEquals(model.array_field[0], 2) def test_array_set_value_list_field(self): - class ArrayModel(BaseModel): array_field = ArrayField(field_type=IntegerField()) @@ -1034,7 +992,6 @@ class ArrayModel(BaseModel): self.assertEqual(0, len(model.array_field)) def test_array_set_value_list_field_valid_and_convertible(self): - class ArrayModel(BaseModel): array_field = ArrayField(field_type=IntegerField()) @@ -1043,7 +1000,6 @@ class ArrayModel(BaseModel): self.assertEqual(1, len(model.array_field)) def test_array_del(self): - class TestModel(BaseModel): field_name_1 = ModelField() field_name_2 = StringField() @@ -1060,7 +1016,6 @@ class ArrayModel(BaseModel): self.assertEqual(0, len(array_model.array_field)) def test_array_model_export_data(self): - class TestModel(BaseModel): field_name_1 = ModelField() field_name_2 = StringField() @@ -1088,7 +1043,6 @@ class ArrayModel(BaseModel): self.assertEqual(expected_data, array_model.export_data()) def test_array_model_export_data_integers(self): - class ArrayModel(BaseModel): array_field = ArrayField(field_type=IntegerField()) @@ -1098,7 +1052,6 @@ class ArrayModel(BaseModel): self.assertEqual({"array_field": [3, 4]}, model.export_data()) def test_array_model_export_data_not_modified(self): - class TestModel(BaseModel): field_name_1 = ModelField() field_name_2 = StringField() @@ -1127,7 +1080,6 @@ class ArrayModel(BaseModel): self.assertEqual(expected_data, array_model.export_data()) def test_array_model_export_data_unitialised(self): - class ArrayModel(BaseModel): array_field = ArrayField(field_type=IntegerField()) @@ -1138,7 +1090,6 @@ class ArrayModel(BaseModel): self.assertEqual({"array_field": []}, model.export_data()) def test_array_model_export_modified_data(self): - class TestModel(BaseModel): field_name_1 = ModelField() field_name_2 = StringField() @@ -1166,7 +1117,6 @@ class ArrayModel(BaseModel): self.assertEqual(expected_data, array_model.export_modified_data()) def test_array_model_export_modified_data_flattered(self): - class TestModel(BaseModel): field_name_1 = ModelField() field_name_2 = StringField() @@ -1191,7 +1141,6 @@ class ArrayModel(BaseModel): self.assertEqual({}, array_model.export_modified_data()) def test_array_model_export_modified_data_integers(self): - class ArrayModel(BaseModel): array_field = ArrayField(field_type=IntegerField()) @@ -1201,7 +1150,6 @@ class ArrayModel(BaseModel): self.assertEqual({"array_field": [3, 4]}, model.export_modified_data()) def test_array_model_export_modified_data_unitialised(self): - class ArrayModel(BaseModel): array_field = ArrayField(field_type=IntegerField()) @@ -1212,7 +1160,6 @@ class ArrayModel(BaseModel): self.assertEqual({"array_field": []}, model.export_modified_data()) def test_array_model_import_data(self): - class ArrayModel(BaseModel): array_field = ArrayField(field_type=IntegerField()) @@ -1221,7 +1168,6 @@ class ArrayModel(BaseModel): self.assertEqual(4, len(array_model.array_field)) def test_array_model_empty(self): - class ArrayModel(BaseModel): array_field = ArrayField(field_type=IntegerField()) @@ -1231,7 +1177,6 @@ class ArrayModel(BaseModel): self.assertListEqual(array_model.array_field.export_data(), []) def test_array_model_with_model_field_no_model_class(self): - class TestModel(BaseModel): field_name_1 = ModelField() field_name_2 = StringField() @@ -1246,7 +1191,6 @@ class ArrayModel(BaseModel): self.assertEqual(list(array_model.array_field), [array_model_indented_1, array_model_indented_2]) def test_array_model_export_modified_data_model_inside(self): - class TestModel(BaseModel): field_name_1 = ModelField() field_name_2 = StringField() @@ -1282,17 +1226,16 @@ def test_array_field_from_desc(self): 'name': None, 'read_only': True})) self.assertEqual(field.export_definition(), { - 'alias': None, - 'doc': 'Array of IntegerField field [READ ONLY]', - 'field_type': (IntegerField, {'alias': None, - 'doc': 'IntegerField field [READ ONLY]', - 'name': None, - 'read_only': True}), - 'name': None, - 'read_only': False}) + 'alias': None, + 'doc': 'Array of IntegerField field [READ ONLY]', + 'field_type': (IntegerField, {'alias': None, + 'doc': 'IntegerField field [READ ONLY]', + 'name': None, + 'read_only': True}), + 'name': None, + 'read_only': False}) def test_hashmap_field(self): - class FakeHashMapModel(HashMapModel): field_name_1 = ModelField() field_name_2 = StringField() @@ -1311,7 +1254,6 @@ class TestModel(BaseModel): self.assertEqual(hash_model.hashmap_field.field_hash_1, 34) def test_hashmap_field_dyn(self): - class TestModel(BaseModel): hashmap_field = HashMapField(field_type=IntegerField()) @@ -1326,10 +1268,10 @@ class TestModel(BaseModel): self.assertEqual(hash_model.hashmap_field.field_hash_1, 34) -class TestArrayOfStringField(TestCase): +class ArrayOfStringFieldTests(TestCase): def setUp(self): - super(TestArrayOfStringField, self).setUp() + super(ArrayOfStringFieldTests, self).setUp() class ArrayModel(BaseModel): array_field = ArrayField(field_type=StringField(), autolist=True) @@ -1354,10 +1296,10 @@ def test_array_field_no_autolist(self): self.assertEqual(self.model.export_data(), {}) -class TestMultiTypeFieldSimpleTypes(TestCase): +class MultiTypeFieldSimpleTypesTests(TestCase): def setUp(self): - super(TestMultiTypeFieldSimpleTypes, self).setUp() + super(MultiTypeFieldSimpleTypesTests, self).setUp() class MultiTypeModel(BaseModel): multi_field = MultiTypeField(field_types=[IntegerField(), StringField()]) @@ -1420,10 +1362,10 @@ def test_multi_field_desc(self): 'read_only': False}) -class TestMultiTypeFieldComplexTypes(TestCase): +class MultiTypeFieldComplexTypesTests(TestCase): def setUp(self): - super(TestMultiTypeFieldComplexTypes, self).setUp() + super(MultiTypeFieldComplexTypesTests, self).setUp() class MultiTypeModel(BaseModel): multi_field = MultiTypeField(field_types=[IntegerField(), (ArrayField, {"field_type": StringField()})]) @@ -1463,10 +1405,10 @@ def test_get_field_type_by_value_fail(self): multi_field.get_field_type_by_value({}) -class TestAutoreferenceModel(TestCase): +class AutoreferenceModelTests(TestCase): def setUp(self): - super(TestAutoreferenceModel, self).setUp() + super(AutoreferenceModelTests, self).setUp() class AutoreferenceModel(BaseModel): multi_field = MultiTypeField(field_types=[IntegerField(), (ArrayField, {"field_type": ModelField()})]) @@ -1483,7 +1425,7 @@ def test_model_reference(self): self.assertIsInstance(self.model.array_of_array[0][0], self.model.__class__) -class TestTimedeltaField(TestCase): +class TimedeltaFieldTests(TestCase): def setUp(self): self.field = TimedeltaField() @@ -1508,3 +1450,134 @@ def test_convert_value_int(self): def test_convert_value_float(self): self.assertTrue(self.field.convert_value(12.11), timedelta(seconds=12, milliseconds=110)) + + +class DateTimeFieldWithTimezoneTests(TestCase): + + def test_no_timezone_none(self): + class Model(BaseModel): + date_time_field = DateTimeField() + + model = Model(date_time_field=datetime(year=2016, month=7, day=21, hour=12, minute=23)) + + self.assertEqual(model.date_time_field, datetime(year=2016, month=7, day=21, hour=12, minute=23)) + self.assertIsNone(model.date_time_field.tzinfo) + + def test_no_timezone_europe(self): + class Model(BaseModel): + date_time_field = DateTimeField() + + model = Model(date_time_field=datetime(year=2016, month=7, day=21, + hour=12, minute=23, tzinfo=tz.gettz('Europe/Amsterdam'))) + + self.assertEqual(model.date_time_field, datetime(year=2016, month=7, day=21, hour=12, minute=23, + tzinfo=tz.gettz('Europe/Amsterdam'))) + self.assertEqual(model.date_time_field.tzinfo, tz.gettz('Europe/Amsterdam')) + + def test_with_default_timezone_utc(self): + class Model(BaseModel): + date_time_field = DateTimeField(default_timezone=timezone.utc) + + model = Model(date_time_field=datetime(year=2016, month=7, day=21, hour=12, minute=23)) + + self.assertEqual(model.date_time_field, datetime(year=2016, month=7, day=21, + hour=12, minute=23, tzinfo=timezone.utc)) + self.assertEqual(model.date_time_field.tzinfo, timezone.utc) + + def test_with_default_timezone_utc_no_changed(self): + class Model(BaseModel): + date_time_field = DateTimeField(default_timezone=timezone.utc) + + model = Model(date_time_field=datetime(year=2016, month=7, day=21, hour=12, minute=23, + tzinfo=tz.gettz('Europe/Amsterdam'))) + + self.assertEqual(model.date_time_field, datetime(year=2016, month=7, day=21, + hour=12, minute=23, tzinfo=tz.gettz('Europe/Amsterdam'))) + self.assertEqual(model.date_time_field.tzinfo, tz.gettz('Europe/Amsterdam')) + + def test_with_default_timezone_europe(self): + class Model(BaseModel): + date_time_field = DateTimeField(default_timezone=tz.gettz('Europe/Amsterdam')) + + model = Model(date_time_field=datetime(year=2016, month=7, day=21, hour=12, minute=23)) + + self.assertEqual(model.date_time_field, datetime(year=2016, month=7, day=21, + hour=12, minute=23, tzinfo=tz.gettz('Europe/Amsterdam'))) + self.assertEqual(model.date_time_field.tzinfo, tz.gettz('Europe/Amsterdam')) + + def test_with_default_force_timezone_utc(self): + class Model(BaseModel): + date_time_field = DateTimeField(default_timezone=timezone.utc, force_timezone=True) + + model = Model(date_time_field=datetime(year=2016, month=7, day=21, + hour=12, minute=23, tzinfo=tz.gettz('Europe/Amsterdam'))) + + self.assertEqual(model.date_time_field, + datetime(year=2016, month=7, day=21, + hour=12, minute=23, + tzinfo=tz.gettz('Europe/Amsterdam')).astimezone(timezone.utc)) + self.assertEqual(model.date_time_field.tzinfo, timezone.utc) + + def test_export_definition(self): + + field = DateTimeField(name='test_field', alias=[], default_timezone=timezone.utc, force_timezone=True) + + self.assertEqual(field.export_definition(), + {'alias': [], 'parse_format': None, + 'doc': 'DateTimeField field', + 'force_timezone': True, + 'default_timezone': timezone.utc, + 'name': 'test_field', 'read_only': False}, + field.export_definition()) + + +class TimeFieldWithTimezone(TestCase): + + def test_no_timezone_none(self): + class Model(BaseModel): + time_field = TimeField() + + model = Model(time_field=time(hour=12, minute=23)) + + self.assertEqual(model.time_field, time(hour=12, minute=23)) + self.assertIsNone(model.time_field.tzinfo) + + def test_no_timezone_europe(self): + class Model(BaseModel): + time_field = TimeField() + + model = Model(time_field=time(hour=12, minute=23, tzinfo=tz.gettz('Europe/Amsterdam'))) + + self.assertEqual(model.time_field, time(hour=12, minute=23, + tzinfo=tz.gettz('Europe/Amsterdam'))) + self.assertEqual(model.time_field.tzinfo, tz.gettz('Europe/Amsterdam')) + + def test_with_default_timezone_utc(self): + class Model(BaseModel): + time_field = TimeField(default_timezone=timezone.utc) + + model = Model(time_field=time(hour=12, minute=23)) + + self.assertEqual(model.time_field, time(hour=12, minute=23, tzinfo=timezone.utc)) + self.assertEqual(model.time_field.tzinfo, timezone.utc) + + def test_with_default_timezone_utc_no_changed(self): + class Model(BaseModel): + time_field = TimeField(default_timezone=timezone.utc) + + model = Model(time_field=time(hour=12, minute=23, + tzinfo=tz.gettz('Europe/Amsterdam'))) + + self.assertEqual(model.time_field, time(hour=12, minute=23, tzinfo=tz.gettz('Europe/Amsterdam'))) + self.assertEqual(model.time_field.tzinfo, tz.gettz('Europe/Amsterdam')) + + def test_export_definition(self): + + field = TimeField(name='test_field', alias=[], default_timezone=timezone.utc) + + self.assertEqual(field.export_definition(), + {'alias': [], 'parse_format': None, + 'doc': 'TimeField field', + 'default_timezone': timezone.utc, + 'name': 'test_field', 'read_only': False}, + field.export_definition()) diff --git a/tests/dirty_models/tests_models.py b/tests/dirty_models/tests_models.py index 60975e0..612bae7 100644 --- a/tests/dirty_models/tests_models.py +++ b/tests/dirty_models/tests_models.py @@ -1,5 +1,6 @@ import pickle from datetime import datetime, date, time, timedelta +from functools import partial from unittest import TestCase from dirty_models.base import Unlocker @@ -1285,20 +1286,17 @@ def test_no_type_def(self): self.assertEqual(model.field1, 'sdsd') def test_import_model(self): - model = FastDynamicModel(data={'test1': 'test_string'}) self.model.import_data(model) self.assertEqual(model.test1, 'test_string') 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) @@ -1315,7 +1313,6 @@ class ModelDefaultValues(BaseModel): class TestDefaultValues(TestCase): def test_field_default_value(self): - model = ModelDefaultValues() self.assertEqual(model.field_integer, 1) @@ -1367,7 +1364,6 @@ def test_modify_model(self): class ModelGeneralDefault(ModelDefaultValues): - __default_data__ = {'field_array_integer': [20, 30, 40], 'field_boolean': False, 'field_datetime': datetime(2017, 11, 23, 23, 56, 59), @@ -1382,7 +1378,6 @@ class ModelGeneralDefault(ModelDefaultValues): class TestGeneralDefaultValues(TestCase): def test_field_default_value(self): - model = ModelGeneralDefault() self.assertEqual(model.field_integer, 9) @@ -1437,9 +1432,7 @@ def test_modify_model(self): class CamelCaseMetaclassTests(TestCase): def test_camelcase_fields(self): - class TestModel(BaseModel, metaclass=CamelCaseMeta): - test_field_1 = StringField() test_field2 = StringField() testField_3 = StringField() @@ -1466,9 +1459,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']) string_field = StringField(name='text_field') @@ -1484,13 +1475,12 @@ class Model(BaseModel): string_field = StringField(name='text_field') self.assertEqual(len(Model.get_structure()), 2) - self.assertIn('scalar_field', Model.get_structure()) + self.assertIn('scalar_field', Model.get_structure()) self.assertIsInstance(Model.get_structure()['scalar_field'], IntegerField) self.assertIn('text_field', Model.get_structure()) self.assertIsInstance(Model.get_structure()['text_field'], StringField) def test_inherit_structure(self): - class Model(BaseModel): integer_field = IntegerField(name='scalar_field', alias=['int_field', 'number_field']) string_field = StringField(name='text_field') @@ -1500,7 +1490,7 @@ class InheritModel(Model): float_field = FloatField() self.assertEqual(len(InheritModel.get_structure()), 3) - self.assertIn('scalar_field', InheritModel.get_structure()) + self.assertIn('scalar_field', InheritModel.get_structure()) self.assertIsInstance(InheritModel.get_structure()['scalar_field'], IntegerField) self.assertIn('text_field', InheritModel.get_structure()) self.assertIsInstance(InheritModel.get_structure()['text_field'], StringField) @@ -1511,7 +1501,6 @@ class InheritModel(Model): class FieldInconsistenceTests(TestCase): def test_override_field(self): - with self.assertRaises(RuntimeError): class Model(BaseModel): test_field_1 = StringField(alias=['text_field']) @@ -1521,7 +1510,6 @@ class Model(BaseModel): class GetAttributeByPathTests(TestCase): def setUp(self): - class InnerModel(BaseModel): test_field_1 = IntegerField() test_field_2 = StringField() @@ -1702,7 +1690,6 @@ def setUp(self): class AvoidInternalAttributesTests(TestCase): def test_hashmap_import_double_underscore(self): - class Model(HashMapModel): test_field = IntegerField() @@ -1711,13 +1698,56 @@ class Model(HashMapModel): self.assertNotEqual(model.__structure__, {}) def test_dynmodel_import_double_underscore(self): - model = DynamicModel() model.import_data({'__class__': 'test_class'}) self.assertNotEqual(model.__class__, 'test_class') def test_fastdynmodel_import_double_underscore(self): - model = FastDynamicModel() model.import_data({'__class__': 'test_class'}) self.assertNotEqual(model.__class__, 'test_class') + + +class ContainsAttributeRegularModelTests(TestCase): + + class Model(HashMapModel): + test_field = IntegerField() + + def test_contains_attribute_original_data(self): + model = self.Model(test_field=1, flat=True) + self.assertTrue('test_field' in model) + + def test_not_contains_attribute(self): + model = self.Model() + + self.assertFalse('test_field' in model) + + def test_not_contains_attribute_original_deleted(self): + model = self.Model(test_field=1, flat=True) + + del model.test_field + + self.assertFalse('test_field' in model) + + def test_contains_attribute_modified_data(self): + model = self.Model(test_field=1) + self.assertTrue('test_field' in model) + + def test_contains_attribute_modified_deleted(self): + model = self.Model(test_field=1) + + del model.test_field + + self.assertFalse('test_field' in model) + + +class ContainsAttributeDynamicModelTests(ContainsAttributeRegularModelTests): + Model = DynamicModel + + +class ContainsAttributeFastDynamicModelTests(ContainsAttributeRegularModelTests): + Model = FastDynamicModel + + +class ContainsAttributeHashMapModelTests(ContainsAttributeRegularModelTests): + Model = partial(HashMapModel, field_type=IntegerField()) diff --git a/tests/dirty_models/tests_types.py b/tests/dirty_models/tests_types.py index 3f92f65..f44afd5 100644 --- a/tests/dirty_models/tests_types.py +++ b/tests/dirty_models/tests_types.py @@ -195,3 +195,30 @@ def test_out_of_bounds_index(self): with self.assertRaises(IndexError): test_list[23] + + +class ContainsItemTests(TestCase): + + def test_contains_item_original_data(self): + + list_model = ListModel([1]) + list_model.flat_data() + + self.assertTrue(1 in list_model) + + def test_contains_item_modified_data(self): + list_model = ListModel([1]) + + self.assertTrue(1 in list_model) + + def test_not_contains_item_original_data(self): + list_model = ListModel([2]) + list_model.flat_data() + + self.assertFalse(1 in list_model) + + def test_not_contains_item_modified_data(self): + list_model = ListModel([1, 2]) + list_model.flat_data() + list_model.pop(0) + self.assertFalse(1 in list_model)