diff --git a/jsonmodels/collections.py b/jsonmodels/collections.py index 950af96..5ec61aa 100644 --- a/jsonmodels/collections.py +++ b/jsonmodels/collections.py @@ -1,5 +1,3 @@ - - class ModelCollection(list): """`ModelCollection` is list which validates stored values. diff --git a/jsonmodels/fields.py b/jsonmodels/fields.py index 722c833..e733273 100644 --- a/jsonmodels/fields.py +++ b/jsonmodels/fields.py @@ -3,7 +3,7 @@ from weakref import WeakKeyDictionary import six -from dateutil.parser import parse +import dateutil.parser from .errors import ValidationError from .collections import ModelCollection @@ -14,6 +14,9 @@ NotSet = object() +_FIELD_NAME_REGEX = re.compile(r'^[A-Za-z_](([\w\-]*)?\w+)?$') + + class BaseField(object): """Base class for all fields.""" @@ -39,6 +42,8 @@ def __init__( self.validate(default) self._default = default + self._initialized = False + @property def has_default(self): return self._default is not NotSet @@ -49,25 +54,30 @@ def _assign_validators(self, validators): self.validators = validators or [] def __set__(self, instance, value): - self._finish_initialization(type(instance)) + if not self._initialized: + self._finish_initialization(type(instance)) + self._initialized = True value = self.parse_value(value) self.validate(value) self.memory[instance._cache_key] = value def __get__(self, instance, owner=None): + if not self._initialized: + if instance is None: + self._finish_initialization(owner) + else: + self._finish_initialization(type(instance)) + self._initialized = True if instance is None: - self._finish_initialization(owner) return self - - self._finish_initialization(type(instance)) - - self._check_value(instance) - return self.memory[instance._cache_key] + else: + self._maybe_assign_default_value(instance) + return self.memory[instance._cache_key] def _finish_initialization(self, owner): pass - def _check_value(self, obj): + def _maybe_assign_default_value(self, obj): if obj._cache_key not in self.memory: self.__set__(obj, self.get_default_value()) @@ -88,10 +98,9 @@ def _check_against_required(self, value): def _validate_against_types(self, value): if value is not None and not isinstance(value, self.types): raise ValidationError( - 'Value is wrong, expected type "{types}"'.format( - types=', '.join([t.__name__ for t in self.types]) - ), - value, + 'Value {value!r} is wrong, expected type {types!r}' + .format(value=value, + types=', '.join(t.__name__ for t in self.types)) ) def _check_types(self): @@ -132,14 +141,15 @@ def get_default_value(self): return self._default if self.has_default else None def _validate_name(self): - if self.name is None: - return - if not re.match('^[A-Za-z_](([\w\-]*)?\w+)?$', self.name): + if self.name is not None \ + and not re.match(_FIELD_NAME_REGEX, self.name): raise ValueError('Wrong name', self.name) - def structue_name(self, default): + def structure_name(self, default): return self.name if self.name is not None else default + structue_name = structure_name + class StringField(BaseField): @@ -159,7 +169,8 @@ def parse_value(self, value): parsed = super(IntField, self).parse_value(value) if parsed is None: return parsed - return int(parsed) + else: + return int(parsed) class FloatField(BaseField): @@ -209,7 +220,7 @@ def _assign_types(self, items_types): try: self.items_types = tuple(items_types) except TypeError: - self.items_types = items_types, + self.items_types = (items_types, ) else: self.items_types = tuple() @@ -244,15 +255,12 @@ def validate_single_value(self, item): def parse_value(self, values): """Cast value to proper collection.""" - result = self.get_default_value() - if not values: - return result - - if not isinstance(values, list): + return self.get_default_value() + elif not isinstance(values, list): return values - - return [self._cast_value(value) for value in values] + else: + return [self._cast_value(value) for value in values] def _cast_value(self, value): if isinstance(value, self.items_types): @@ -271,11 +279,11 @@ def _finish_initialization(self, owner): super(ListField, self)._finish_initialization(owner) types = [] - for type in self.items_types: - if isinstance(type, _LazyType): - types.append(type.evaluate(owner)) + for type_ in self.items_types: + if isinstance(type_, _LazyType): + types.append(type_.evaluate(owner)) else: - types.append(type) + types.append(type_) self.items_types = tuple(types) def _elem_to_struct(self, value): @@ -330,9 +338,9 @@ def parse_value(self, value): """Parse value to proper model type.""" if not isinstance(value, dict): return value - - embed_type = self._get_embed_type() - return embed_type(**value) + else: + embed_type = self._get_embed_type() + return embed_type(**value) def _get_embed_type(self): if len(self.types) != 1: @@ -419,9 +427,10 @@ def parse_value(self, value): """Parse string into instance of `time`.""" if value is None: return value - if isinstance(value, datetime.time): + elif isinstance(value, datetime.time): return value - return parse(value).timetz() + else: + return dateutil.parser.parse(value).timetz() class DateField(StringField): @@ -451,9 +460,10 @@ def parse_value(self, value): """Parse string into instance of `date`.""" if value is None: return value - if isinstance(value, datetime.date): + elif isinstance(value, datetime.date): return value - return parse(value).date() + else: + return dateutil.parser.parse(value).date() class DateTimeField(StringField): @@ -482,7 +492,7 @@ def parse_value(self, value): """Parse string into instance of `datetime`.""" if isinstance(value, datetime.datetime): return value - if value: - return parse(value) + elif value: + return dateutil.parser.parse(value) else: return None diff --git a/jsonmodels/models.py b/jsonmodels/models.py index da4bbac..89674f6 100644 --- a/jsonmodels/models.py +++ b/jsonmodels/models.py @@ -19,10 +19,10 @@ def validate_fields(attributes): } taken_names = set() for name, field in fields.items(): - structue_name = field.structue_name(name) - if structue_name in taken_names: - raise ValueError('Name taken', structue_name, name) - taken_names.add(structue_name) + structure_name = field.structure_name(name) + if structure_name in taken_names: + raise ValueError('Name taken', structure_name, name) + taken_names.add(structure_name) class Base(six.with_metaclass(JsonmodelMeta, object)): @@ -31,61 +31,66 @@ class Base(six.with_metaclass(JsonmodelMeta, object)): def __init__(self, **kwargs): self._cache_key = _CacheKey() + self.initialize_fields() self.populate(**kwargs) + def initialize_fields(self): + for _, _, _ in self.iterate_with_name(): + pass + def populate(self, **values): """Populate values to fields. Skip non-existing.""" values = values.copy() - fields = list(self.iterate_with_name()) - for _, structure_name, field in fields: + for attr_name, structure_name, field in self.iterate_with_name(): + # set field by structure name if structure_name in values: field.__set__(self, values.pop(structure_name)) - for name, _, field in fields: - if name in values: - field.__set__(self, values.pop(name)) + elif attr_name in values: + field.__set__(self, values.pop(attr_name)) - def get_field(self, field_name): + @classmethod + def get_field(cls, field_name): """Get field associated with given attribute.""" - for attr_name, field in self: - if field_name == attr_name: - return field - - raise errors.FieldNotFound('Field not found', field_name) + field = getattr(cls, field_name, None) + if isinstance(field, BaseField): + return field + else: + raise errors.FieldNotFound('Field not found', field_name) def __iter__(self): """Iterate through fields and values.""" - for name, field in self.iterate_over_fields(): - yield name, field + for item in self.iterate_over_fields(): + yield item def validate(self): """Explicitly validate all the fields.""" - for name, field in self: + for attr_name, field in self: try: field.validate_for_object(self) except ValidationError as error: raise ValidationError( - "Error for field '{name}'.".format(name=name), - error, + 'Error for field {attr_name!r}: {error}' + .format(attr_name=attr_name, error=error) ) @classmethod def iterate_over_fields(cls): """Iterate through fields as `(attribute_name, field_instance)`.""" - for attr in dir(cls): - clsattr = getattr(cls, attr) - if isinstance(clsattr, BaseField): - yield attr, clsattr + for attr_name in dir(cls): + field = getattr(cls, attr_name) + if isinstance(field, BaseField): + yield attr_name, field @classmethod def iterate_with_name(cls): """Iterate over fields, but also give `structure_name`. - Format is `(attribute_name, structue_name, field_instance)`. + Format is `(attribute_name, structure_name, field_instance)`. Structure name is name under which value is seen in structure and schema (in primitives) and only there. """ for attr_name, field in cls.iterate_over_fields(): - structure_name = field.structue_name(attr_name) + structure_name = field.structure_name(attr_name) yield attr_name, structure_name, field def to_struct(self): @@ -99,11 +104,11 @@ def to_json_schema(cls): def __repr__(self): attrs = {} - for name, _ in self: + for attr_name, field in self: try: - attr = getattr(self, name) + attr = getattr(self, attr_name) if attr is not None: - attrs[name] = repr(attr) + attrs[attr_name] = repr(attr) except ValidationError: pass @@ -130,14 +135,14 @@ def __eq__(self, other): if type(other) is not type(self): return False - for name, _ in self.iterate_over_fields(): + for attr_name, _ in self.iterate_over_fields(): try: - our = getattr(self, name) + our = getattr(self, attr_name) except errors.ValidationError: our = None try: - their = getattr(other, name) + their = getattr(other, attr_name) except errors.ValidationError: their = None @@ -152,3 +157,4 @@ def __ne__(self, other): class _CacheKey(object): """Object to identify model in memory.""" + pass diff --git a/tests/test_utilities.py b/tests/test_utilities.py index b82d5c6..f9262b1 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -59,56 +59,56 @@ def test_failed_comparison_of_two_dicts(): def test_is_ecma_regex(): - assert utilities.is_ecma_regex('some regex') is False - assert utilities.is_ecma_regex('^some regex$') is False - assert utilities.is_ecma_regex('/^some regex$/') is True - assert utilities.is_ecma_regex('/^some regex$/gim') is True - assert utilities.is_ecma_regex('/^some regex$/trololo') is True + assert utilities.is_ecma_regex(r'some regex') is False + assert utilities.is_ecma_regex(r'^some regex$') is False + assert utilities.is_ecma_regex(r'/^some regex$/') is True + assert utilities.is_ecma_regex(r'/^some regex$/gim') is True + assert utilities.is_ecma_regex(r'/^some regex$/trololo') is True with pytest.raises(ValueError): - utilities.is_ecma_regex('/wrong regex') + utilities.is_ecma_regex(r'/wrong regex') with pytest.raises(ValueError): - utilities.is_ecma_regex('wrong regex/') + utilities.is_ecma_regex(r'wrong regex/') with pytest.raises(ValueError): - utilities.is_ecma_regex('wrong regex/gim') + utilities.is_ecma_regex(r'wrong regex/gim') with pytest.raises(ValueError): - utilities.is_ecma_regex('wrong regex/asdf') + utilities.is_ecma_regex(r'wrong regex/asdf') - assert utilities.is_ecma_regex('/^some regex\/gim') is True + assert utilities.is_ecma_regex(r'/^some regex\/gim') is True - assert utilities.is_ecma_regex('/^some regex\\\\/trololo') is True - assert utilities.is_ecma_regex('/^some regex\\\\\/gim') is True - assert utilities.is_ecma_regex('/\\\\/') is True + assert utilities.is_ecma_regex(r'/^some regex\\/trololo') is True + assert utilities.is_ecma_regex(r'/^some regex\\\/gim') is True + assert utilities.is_ecma_regex(r'/\\/') is True - assert utilities.is_ecma_regex('some /regex/asdf') is False - assert utilities.is_ecma_regex('^some regex$//') is False + assert utilities.is_ecma_regex(r'some /regex/asdf') is False + assert utilities.is_ecma_regex(r'^some regex$//') is False def test_convert_ecma_regex_to_python(): - assert ('some', []) == utilities.convert_ecma_regex_to_python('/some/') + assert ('some', []) == utilities.convert_ecma_regex_to_python(r'/some/') assert ( ('some/pattern', []) == - utilities.convert_ecma_regex_to_python('/some/pattern/') + utilities.convert_ecma_regex_to_python(r'/some/pattern/') ) assert ( - ('^some \d+ pattern$', []) == - utilities.convert_ecma_regex_to_python('/^some \d+ pattern$/') + (r'^some \d+ pattern$', []) == + utilities.convert_ecma_regex_to_python(r'/^some \d+ pattern$/') ) - regex, flags = utilities.convert_ecma_regex_to_python('/^regex \d/i') - assert '^regex \d' == regex + regex, flags = utilities.convert_ecma_regex_to_python(r'/^regex \d/i') + assert r'^regex \d' == regex assert set([re.I]) == set(flags) - result = utilities.convert_ecma_regex_to_python('/^regex \d/m') - assert '^regex \d' == result.regex + result = utilities.convert_ecma_regex_to_python(r'/^regex \d/m') + assert r'^regex \d' == result.regex assert set([re.M]) == set(result.flags) - result = utilities.convert_ecma_regex_to_python('/^regex \d/mi') - assert '^regex \d' == result.regex + result = utilities.convert_ecma_regex_to_python(r'/^regex \d/mi') + assert r'^regex \d' == result.regex assert set([re.M, re.I]) == set(result.flags) with pytest.raises(ValueError): - utilities.convert_ecma_regex_to_python('/regex/wrong') + utilities.convert_ecma_regex_to_python(r'/regex/wrong') assert ( ('python regex', []) == @@ -116,8 +116,8 @@ def test_convert_ecma_regex_to_python(): ) assert ( - ('^another \d python regex$', []) == - utilities.convert_ecma_regex_to_python('^another \d python regex$') + (r'^another \d python regex$', []) == + utilities.convert_ecma_regex_to_python(r'^another \d python regex$') ) result = utilities.convert_ecma_regex_to_python('python regex') @@ -127,61 +127,61 @@ def test_convert_ecma_regex_to_python(): def test_convert_python_regex_to_ecma(): assert ( - '/^some regex$/' == - utilities.convert_python_regex_to_ecma('^some regex$') + r'/^some regex$/' == + utilities.convert_python_regex_to_ecma(r'^some regex$') ) assert ( - '/^some regex$/' == - utilities.convert_python_regex_to_ecma('^some regex$', []) + r'/^some regex$/' == + utilities.convert_python_regex_to_ecma(r'^some regex$', []) ) assert ( - '/pattern \d+/i' == - utilities.convert_python_regex_to_ecma('pattern \d+', [re.I]) + r'/pattern \d+/i' == + utilities.convert_python_regex_to_ecma(r'pattern \d+', [re.I]) ) assert ( - '/pattern \d+/m' == - utilities.convert_python_regex_to_ecma('pattern \d+', [re.M]) + r'/pattern \d+/m' == + utilities.convert_python_regex_to_ecma(r'pattern \d+', [re.M]) ) assert ( - '/pattern \d+/im' == - utilities.convert_python_regex_to_ecma('pattern \d+', [re.I, re.M]) + r'/pattern \d+/im' == + utilities.convert_python_regex_to_ecma(r'pattern \d+', [re.I, re.M]) ) assert ( - '/ecma pattern$/' == - utilities.convert_python_regex_to_ecma('/ecma pattern$/') + r'/ecma pattern$/' == + utilities.convert_python_regex_to_ecma(r'/ecma pattern$/') ) assert ( - '/ecma pattern$/im' == - utilities.convert_python_regex_to_ecma('/ecma pattern$/im') + r'/ecma pattern$/im' == + utilities.convert_python_regex_to_ecma(r'/ecma pattern$/im') ) assert ( - '/ecma pattern$/wrong' == - utilities.convert_python_regex_to_ecma('/ecma pattern$/wrong') + r'/ecma pattern$/wrong' == + utilities.convert_python_regex_to_ecma(r'/ecma pattern$/wrong') ) assert ( - '/ecma pattern$/m' == - utilities.convert_python_regex_to_ecma('/ecma pattern$/m', [re.M]) + r'/ecma pattern$/m' == + utilities.convert_python_regex_to_ecma(r'/ecma pattern$/m', [re.M]) ) def test_converters(): assert ( - '/^ecma \d regex$/im' == + r'/^ecma \d regex$/im' == utilities.convert_python_regex_to_ecma( - *utilities.convert_ecma_regex_to_python('/^ecma \d regex$/im')) + *utilities.convert_ecma_regex_to_python(r'/^ecma \d regex$/im')) ) result = utilities.convert_ecma_regex_to_python( utilities.convert_python_regex_to_ecma( - '^some \w python regex$', [re.I])) + r'^some \w python regex$', [re.I])) - assert '^some \w python regex$' == result.regex + assert r'^some \w python regex$' == result.regex assert [re.I] == result.flags