Skip to content

Commit

Permalink
Implemented protected fields interface via Document.create(...)
Browse files Browse the repository at this point in the history
  • Loading branch information
prawn-cake committed Aug 20, 2016
1 parent 89297aa commit 3e37cc2
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 89 deletions.
89 changes: 10 additions & 79 deletions Changelog → Changelog.md
Original file line number Diff line number Diff line change
@@ -1,162 +1,93 @@
Changelog
=========

0.4.1 (2016-06-01)
0.4.1+1 (UNRELEASED)
--------------------
* [Fix] Fix OMIT_MISSED_FIELDS behavior
* [Improvement] Follow consistency and raise ValidationError instead of ValueError on choices validation if failed
* [Improvement] Protected fields interface via Document.create(...)

0.4.1 (2016-06-01)
------------------
* [Bugfix] Fix OMIT_MISSED_FIELDS behavior
* [Improvement] Follow consistency and raise ValidationError instead of ValueError on choices validation if failed

0.4.0 (2016-04-08)
--------------------

------------------
* [Feature] Implemented Document.Meta AttributeDict based options
* [Improvement] Renamed Meta.OMIT_NOT_PASSED_FIELDS -> Meta.OMIT_MISSED_FIELDS
* [Feature] Post-init model validation
* [Feature] New DictField


0.3.7 (2015-12-11)
-----------------

------------------
* New SimpleField required attribute handling (moved to validate() method)

* Implemented documents multiple inheritance

* More wise fields descriptors setters strategy on init and on __setattr__

* Split up SimpleField validation chain into several steps to adapt it
* for better overriding in case of inheritance (cleaner ListField
* implementation)


0.3.6 (2015-12-11)
------------------

* Fixed OMIT_NOT_PASSED_FIELDS to work with defaults

* Updated SimpleField.required attribute handling;
* Implemented models inheritance, refactoring DocumentMeta class;
* Significant improvements in SimpleField.validate method - divide it into composition methods;


0.3.5 (2015-12-10)
------------------

* Fixed CharField max_length validator; fixed AttributeDict setter

* Use common SimpleField value setter for descriptors and for Document init.
* Updated CharField, forbid to store None value, store '' instead; Simplified Document init


0.3.4 (2015-12-09)
------------------

* Implemented Document.IGNORE_NONE_ON_INIT feature.
* Code clean up and minor fixes


0.3.3 (2015-11-26)
------------------------

------------------
* Allow None values for non required fields. [Maksim Ekimovskii]

* Code refactoring: moved helpers to utils.py, removed helpers.py, minor
renamings and clean up. [Maksim Ekimovskii]


0.3.2 (2015-10-10)
------------------

* Implement ListField, make it json serialize compatible for python2/3; Fixed
some of the bugs for default field value; Added experimental validator wrapper is_instance;
Raise AttributeError instead of KeyError for AttributeDict; [Maksim Ekimovskii]


0.3.1 (2015-10-09)
------------------

* Initialize callable default values at document init point; Added new
tests. [Maksim Ekimovskii]

* Implemented ListField, added tests; Forbid mutable default parameters;
Remove redundant code. [Maksim Ekimovskii]


0.3.0 (2015-09-27)
------------------

* Implemented choices feature, removed redundant code, updated tests and
README. [Maksim Ekimovskii]


0.2.5 (2015-09-19)
------------------

* Implemented ALLOW_EXTRA_FIELDS document attribute; Improve python 3
compatibility. [Maksim Ekimovskii]

* Fixed README. [Maksim Ekimovskii]

* Implemented new validators pipeline; Added new immutable field option;
Fixed nested behaviour; Updated tests. [Maksim Ekimovskii]

* Validate model defaults; Fixed working with nested models; Added
ImmutableDocument with immutable fields; Code refactoring: rename
DictEmbeddedDocument --> Document with backward compatibility; Added
unittests. [Maksim Ekimovskii]

* Replace SimpleField get_name() with property name. [Maksim Ekimovskii]

* Remove get_instance constructor; Fixed tests. [Maksim Ekimovskii]

* Code refactoring, remove deprecated type parameter from SimpleField;
Added typed field types: IntegerField, Float, etc; Optimize
validations; Added tests. [Maksim Ekimovskii]


0.2.4 (2015-04-25)
------------------

* Implemented new feature - optional SimpleField name; Added tests;
Minor fixes; Refactoring. [Maksim Ekimovskii]

* Exception refactoring. [Maksim Ekimovskii]

* Added new field attribute 'error_text' to help to debug validation
error messages; Distinguish default values whether callable or not;
Minor refactoring; [Maksim Ekimovskii]

* Fixed DictEmbeddedDocument set default functionality + validation;
Updated tests. [Maksim Ekimovskii]

* Integrate six lib to python 2 to 3 compatibility. [Maksim Ekimovskii]


0.2.1 (2015-02-12)
------------------

* Updated docs; added test_coverage make commandl updated travis cfg.
[Maksim Ekimovskii]

* Added new SimpleField validator feature -> it will be replace 'type'
parameter; Updated meta models, added field holder name; Splitted
validation, moved it to SimpleField.validator; Added new tests;
Refactoring, cleanup and docstrings coverage. [Maksim Ekimovskii]

* Added strip_kwargs method for DictEmbeddedDocument. [Maksim
Ekimovskii]

* Models refactoring, improve readability; added .coveragerc. [Maksim
Ekimovskii]

* Rename _type -> type SimpleField attribute. [Maksim Ekimovskii]

* Fix DictEmbeddedDocument type casting; Updated tests; Updated
documentation. [Maksim Ekimovskii]

* Updated validators, integrate it to field setter and document
initializator. [Maksim Ekimovskii]

* Rewritten field storage logic to prevent memory leaks, store all
instance values in __dict__ [Maksim Ekimovskii]

4 changes: 3 additions & 1 deletion simplemodels/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class SimpleField(object):
def __init__(self, default=None, required=False, choices=None, name=None,
validators=None, error_text='', immutable=False, **kwargs):
"""
:param name: optional name
:param name: field name, it's set in the DocumentMeta
:param default: default value
:param required: is field required
:param choices: choices list.
Expand Down Expand Up @@ -323,6 +323,8 @@ def __init__(self, item_types, **kwargs):

super(ListField, self).__init__(**kwargs)

self._set_default_value(kwargs.get('default', []))

def _pre_validate(self, value, err=ValidationError):
"""Custom list field validate method
Expand Down
63 changes: 54 additions & 9 deletions simplemodels/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-

import six
import inspect
import weakref
from simplemodels.exceptions import ImmutableDocumentError, \
ModelValidationError
from simplemodels.fields import SimpleField, DocumentField
Expand All @@ -9,6 +11,9 @@
__all__ = ['Document', 'ImmutableDocument']


registry = weakref.WeakValueDictionary()


class AttributeDict(dict):

"""Dict wrapper with access to keys via attributes"""
Expand Down Expand Up @@ -66,6 +71,7 @@ def __new__(mcs, name, parents, dct):
obj._name = field_name
obj._holder_name = name # class holder name
_fields[obj.name] = obj

elif all([field_name == 'Meta', inspect.isclass(obj)]):
_meta.update(obj.__dict__)

Expand All @@ -74,6 +80,7 @@ def __new__(mcs, name, parents, dct):
dct['_meta'] = _meta

cls = super(DocumentMeta, mcs).__new__(mcs, name, parents, dct)
registry[name] = cls
return cls

def __instancecheck__(cls, instance):
Expand Down Expand Up @@ -110,40 +117,53 @@ class Meta:
# field is given for the document

def __init__(self, **kwargs):
kwargs = self._clean_kwargs(kwargs)
# This flag set by .create(...) method
# kwargs = self._clean_kwargs(kwargs)

# dict init
prepared_fields = self._prepare_fields(**kwargs)
prepared_fields = self._prepare_fields(kwargs)
super(Document, self).__init__(**prepared_fields)
self._post_init_validation()

def _prepare_fields(self, **kwargs):
def _prepare_fields(self, kwargs):
"""Do field validations and set defaults
:param kwargs: init parameters
:return: :raise RequiredValidationError:
"""

# prepare protected
prepared = {}
protect_prefix = '%s_' % self.__class__.__name__
for k, v in kwargs.items():
if k.startswith(protect_prefix):
k = k.replace(protect_prefix, '')
prepared[k] = v

# Do not override protected and decoded values
if k not in prepared:
prepared[k] = v

# It validates values on set, see
# simplemodels.fields.SimpleField#__set_value__
for field_name, field_obj in self._fields.items():

# Get field value or set default
default_val = getattr(field_obj, 'default')
field_val = kwargs.get(field_name)
field_val = prepared.get(field_name)
if field_val is None:
field_val = default_val() if callable(default_val) \
else default_val

# Build model structure
if field_name in kwargs:
if field_name in prepared:
# set presented field
val = field_obj.__set_value__(self, field_val)
kwargs[field_name] = val
prepared[field_name] = val
elif issubclass(type(field_obj), DocumentField):
# build empty nested document
val = field_obj.__set_value__(self, {})
kwargs[field_name] = val
prepared[field_name] = val
else:
# field is not presented in the given init parameters
if field_val is None and self._meta.OMIT_MISSED_FIELDS:
Expand All @@ -152,12 +172,18 @@ def _prepare_fields(self, **kwargs):
field_obj.validate(field_val)
continue
val = field_obj.__set_value__(self, field_val)
kwargs[field_name] = val
prepared[field_name] = val

return kwargs
return prepared

@classmethod
def _clean_kwargs(cls, kwargs):
"""Clean with excluding extra fields if the model has
ALLOW_EXTRA_FIELDS meta flag on
:param kwargs: dict
:return: cleaned kwargs
"""
fields = getattr(cls, '_fields', {})

# put everything extra in the document
Expand Down Expand Up @@ -185,6 +211,25 @@ def _post_init_validation(self):
'%s (%r) is not a function' %
(method_name, validation_method, ))

@classmethod
def _protect_fields(cls, kwargs):
"""Rename all of the fields with ModelName_field.
This enables fields protection from 'self', 'cls' and other reserved
keywords
"""
return {'%s_%s' % (cls.__name__, k): v for k, v in kwargs.items()}

@classmethod
def create(cls, kwargs):
"""Safe factory method to create a document
:param kwargs:
:return:
"""
kwargs = cls._clean_kwargs(kwargs)
protected = cls._protect_fields(kwargs)
return cls(**protected)


class ImmutableDocument(Document):
"""Read only document. Useful for validation purposes only"""
Expand Down
3 changes: 3 additions & 0 deletions simplemodels/tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ class Post(Document):
post = Post(text='test', tags=['a', 1, Decimal(1)])
self.assertIsNone(post)

post = Post(text='text')
self.assertEqual(post.tags, [])

post = Post(text='test', tags=['a', 1.0])
self.assertTrue(post)

Expand Down
25 changes: 25 additions & 0 deletions simplemodels/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,31 @@ def validate_password(document, value):
user = User(name='Mikko', password='1234567890', is_admin=True)
self.assertIsInstance(user, User)

def test_model_with_self_field(self):
class User(Document):
class Meta:
ALLOW_EXTRA_FIELDS = True

name = CharField()
self = CharField()
cls = CharField()

class Company(Document):
name = CharField()

data = {
'name': 'John Smith',
'self': 'Handsome',
'cls': 'So fresh, So clean clean',
'unused': 'foo/bar'
}

user = User.create(data)
self.assertIsInstance(user, User)
self.assertEqual(user, data)
company = Company.create(data)
self.assertIsInstance(company, Company)


class DocumentMetaOptionsTest(TestCase):
def test_nested_meta(self):
Expand Down

0 comments on commit 3e37cc2

Please sign in to comment.