diff --git a/Makefile b/Makefile index e5ffd5c..77b06aa 100644 --- a/Makefile +++ b/Makefile @@ -31,9 +31,9 @@ run-tests: @echo "Running tests..." nosetests --with-coverage -d --cover-package=dirty_models --cover-erase -x -publish: +publish: clean build @echo "Publishing new version on Pypi..." - python setup.py bdist_wheel upload + twine upload dist/* clean: @echo "Cleaning compiled files..." diff --git a/README.rst b/README.rst index aa2c1f2..ee917cb 100644 --- a/README.rst +++ b/README.rst @@ -85,6 +85,14 @@ Features Changelog --------- +Version 0.11.3 +-------------- + +- Fix bug casting string negative float. +- Fix exception casting non valid values to enumerations. +- Added `title` property to fields. +- Added `metadata` property to fields. It could be used to store anything. +- Improved model formatter. Version 0.11.2 -------------- diff --git a/dirty_models/__init__.py b/dirty_models/__init__.py index 656a78a..2c9215d 100644 --- a/dirty_models/__init__.py +++ b/dirty_models/__init__.py @@ -8,4 +8,4 @@ from .fields import * from .utils import * -__version__ = '0.11.2' +__version__ = '0.11.3' diff --git a/dirty_models/fields.py b/dirty_models/fields.py index e53d928..915c0aa 100644 --- a/dirty_models/fields.py +++ b/dirty_models/fields.py @@ -2,13 +2,13 @@ Fields to be used with dirty models. """ -from datetime import date, datetime, time, timedelta - from collections import Mapping -from dateutil.parser import parse as dateutil_parse +from datetime import date, datetime, time, timedelta from enum import Enum from functools import wraps +from dateutil.parser import parse as dateutil_parse + from .model_types import ListModel __all__ = ['IntegerField', 'FloatField', 'BooleanField', 'StringField', 'StringIdField', @@ -19,12 +19,14 @@ class BaseField: """Base field descriptor.""" - def __init__(self, name=None, alias=None, getter=None, setter=None, read_only=False, default=None, doc=None): + def __init__(self, name=None, alias=None, getter=None, setter=None, read_only=False, + default=None, title=None, doc=None): self._name = None self.name = name self.alias = alias self.read_only = read_only self.default = default + self.title = title self._getter = getter self._setter = setter self.__doc__ = doc or self.get_field_docstring() @@ -212,9 +214,12 @@ 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()) + try: + float(value) + except (ValueError, TypeError): + return False + else: + return True class BooleanField(BaseField): @@ -642,12 +647,13 @@ class than model who define field. """ def __init__(self, model_class=None, **kwargs): - self._model_class = None - self.model_class = model_class - self._model_setter = None - if 'setter' in kwargs: - self._model_setter = kwargs['setter'] - del (kwargs['setter']) + self._model_class = model_class + + try: + self._model_setter = kwargs.pop('setter') + except KeyError: + self._model_setter = None + super(ModelField, self).__init__(**kwargs) def export_definition(self): @@ -669,6 +675,7 @@ def model_class(self): @model_class.setter def model_class(self, model_class): """Model_class setter: model class used on field""" + self._model_class = model_class def convert_value(self, value): @@ -930,7 +937,10 @@ def can_use_value(self, value): except ValueError: pass - return value in self.enum_class.__members__.keys() + try: + return value in self.enum_class.__members__.keys() + except Exception: + return False class BytesField(BaseField): diff --git a/dirty_models/utils.py b/dirty_models/utils.py index 955f479..0247dbe 100644 --- a/dirty_models/utils.py +++ b/dirty_models/utils.py @@ -1,8 +1,9 @@ -from datetime import date, datetime, time, timedelta +from abc import abstractmethod +from enum import Enum from json.encoder import JSONEncoder as BaseJSONEncoder import re -from enum import Enum +from datetime import date, datetime, time, timedelta from .fields import MultiTypeField from .model_types import ListModel @@ -52,7 +53,10 @@ def keys(self): class BaseFormatterIter: - pass + + @abstractmethod + def format(self): # pragma: no cover + pass class BaseFieldtypeFormatterIter(BaseFormatterIter): @@ -69,6 +73,9 @@ def __iter__(self): for item in self.obj: yield self.parent_formatter.format_field(self.field, item) + def format(self): + return list(self) + class BaseModelFormatterIter(BaseModelIterator, BaseFormatterIter): """ @@ -92,6 +99,9 @@ def format_field(self, field, value): return value + def format(self): + return {k: v.format() if isinstance(v, BaseFormatterIter) else v for k, v in self} + class ModelFormatterIter(BaseModelFormatterIter): """ @@ -119,11 +129,11 @@ class JSONEncoder(BaseJSONEncoder): def default(self, obj): if isinstance(obj, BaseModel): - 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) + return self.default(self.default_model_iter(obj)) + elif isinstance(obj, BaseFormatterIter): + return obj.format() + else: + return super(JSONEncoder, self).default(obj) class Factory: diff --git a/tests/dirty_models/tests_docs.py b/tests/dirty_models/tests_docs.py new file mode 100644 index 0000000..1c66763 --- /dev/null +++ b/tests/dirty_models/tests_docs.py @@ -0,0 +1,32 @@ +from unittest import TestCase, skip + +from dirty_models import BaseModel, IntegerField + + +class TestModel(BaseModel): + inner_doc_field = IntegerField(doc='Inner doc') + + post_doc_field = IntegerField() + """Post doc""" + + #: Pre doc + pre_doc_field = IntegerField() + + titled_field = IntegerField(title='Titled field') + + +class DocStringTests(TestCase): + + def test_inner_docstring(self): + self.assertEquals(TestModel.inner_doc_field.__doc__, 'Inner doc') + + @skip('No way') + def test_post_docstring(self): + self.assertEquals(TestModel().post_doc_field.__doc__, 'Post doc', TestModel().post_doc_field.__doc__) + + @skip('No way') + def test_pre_docstring(self): + self.assertEquals(TestModel().pre_doc_field.__doc__, 'Pre doc', TestModel().pre_doc_field.__doc__) + + def test_title(self): + self.assertEquals(TestModel.titled_field.title, 'Titled field') diff --git a/tests/dirty_models/tests_fields.py b/tests/dirty_models/tests_fields.py index dccfc97..57374ef 100644 --- a/tests/dirty_models/tests_fields.py +++ b/tests/dirty_models/tests_fields.py @@ -1,10 +1,10 @@ +import sys from datetime import date, datetime, time, timedelta, timezone +from enum import Enum from unittest import TestCase import iso8601 -import sys from dateutil import tz -from enum import Enum from dirty_models.fields import ArrayField, BooleanField, BytesField, DateField, DateTimeField, EnumField, FloatField, \ HashMapField, IntegerField, ModelField, MultiTypeField, StringField, StringIdField, TimeField, TimedeltaField @@ -31,6 +31,12 @@ def test_float_field_using_str(self): self.assertTrue(field.can_use_value("3.0")) self.assertEqual(field.use_value("3.0"), 3.0) + def test_float_field_using_str_negative(self): + field = FloatField() + self.assertFalse(field.check_value("-3.0")) + self.assertTrue(field.can_use_value("-3.0")) + self.assertEqual(field.use_value("-3.0"), -3.0) + def test_float_field_using_dict(self): field = FloatField() self.assertFalse(field.check_value({})) @@ -1294,7 +1300,6 @@ def test_array_field_no_autolist(self): class IntegerFieldTests(TestCase): - class TestEnum(Enum): value_1 = 1 value_2 = '2' @@ -1645,7 +1650,6 @@ def test_export_definition(self): class EnumFieldTests(TestCase): - class TestEnum(Enum): value_1 = 'value1' value_2 = 2 @@ -1690,6 +1694,54 @@ def test_export_definition(self): 'name': 'test_field', 'read_only': False}, self.field.export_definition()) + def test_export_data(self): + class Model(BaseModel): + field = EnumField(enum_class=self.TestEnum) + + model = Model(field=self.TestEnum.value_1) + + self.assertEqual(model.export_data(), {'field': self.TestEnum.value_1}) + + def test_multitype_export_data(self): + class Model(BaseModel): + field = MultiTypeField(field_types=[EnumField(enum_class=self.TestEnum), + ArrayField(field_type=EnumField(enum_class=self.TestEnum))]) + + model = Model() + model.field = self.TestEnum.value_1 + + self.assertEqual(model.export_data(), {'field': self.TestEnum.value_1}) + + def test_multitype_export_data_inverted(self): + class Model(BaseModel): + field = MultiTypeField(field_types=[ArrayField(field_type=EnumField(enum_class=self.TestEnum)), + EnumField(enum_class=self.TestEnum)]) + + model = Model() + model.field = self.TestEnum.value_1 + + self.assertEqual(model.export_data(), {'field': self.TestEnum.value_1}) + + def test_multitype_export_data_array(self): + class Model(BaseModel): + field = MultiTypeField(field_types=[EnumField(enum_class=self.TestEnum), + ArrayField(field_type=EnumField(enum_class=self.TestEnum))]) + + model = Model() + model.field = [self.TestEnum.value_1, ] + + self.assertEqual(model.export_data(), {'field': [self.TestEnum.value_1, ]}) + + def test_multitype_export_data_array_inverted(self): + class Model(BaseModel): + field = MultiTypeField(field_types=[ArrayField(field_type=EnumField(enum_class=self.TestEnum)), + EnumField(enum_class=self.TestEnum)]) + + model = Model() + model.field = [self.TestEnum.value_1, ] + + self.assertEqual(model.export_data(), {'field': [self.TestEnum.value_1, ]}) + class BytesFieldTests(TestCase): diff --git a/tests/dirty_models/tests_utils.py b/tests/dirty_models/tests_utils.py index 48d1267..a7db463 100644 --- a/tests/dirty_models/tests_utils.py +++ b/tests/dirty_models/tests_utils.py @@ -1,8 +1,8 @@ -from datetime import date, datetime, timedelta +from enum import Enum from json import dumps, loads from unittest.case import TestCase -from enum import Enum +from datetime import date, datetime, timedelta from dirty_models.fields import ArrayField, DateField, DateTimeField, EnumField, HashMapField, IntegerField, \ ModelField, MultiTypeField, StringIdField, TimedeltaField @@ -191,11 +191,26 @@ def test_model_json(self): self.assertEqual(loads(json_str), data) + def test_model_json_enum_str(self): + model = TestModel(data={'test_enum': TestModel.TestEnum.value_2}) + + json_str = dumps(model, cls=JSONEncoder) + + data = {'test_enum': '2'} + + 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) + def test_fail_unknown_type(self): + data = {'foo': {2, 3}} + + with self.assertRaises(TypeError): + dumps(data, cls=JSONEncoder) + class ModelIteratorTests(TestCase):