import copy
import datetime
import six
from bson import DBRef, ObjectId
from .errors import ValidationError
from .util import check_keys
[docs]class Field(object):
"""Instances of this class is used to define field types and automatically
create validators::
field_name = Field(str, default='cheeseburger')
foo = Field(datetime, auto_update=True)
bar = Field(list, required=False)
"""
allowed_types = (bool, int, float, six.binary_type, six.text_type,
list, dict, datetime.datetime, DBRef, ObjectId)
# kwarg_name : kwarg_input_validator dictionaries
allowed_kwargs = {
'default': lambda v: True,
'required': lambda v: isinstance(v, bool),
}
extra_kwargs = {
datetime.datetime: {'auto_update': lambda v: isinstance(v, bool)},
DBRef: {'document_class': lambda v: isinstance(v, six.string_types)},
}
[docs] def __init__(self, *args, **kwargs):
"""Field kwargs are checked for correctness and field validator is set,
along with other attributes such as ``required`` and ``auto_update``
:Keyword Arguments:
- `default`: default field value, must pass type check, can be a ``callable``
- `required`: if ``True`` field must exist and not be ``None`` (default: ``True``)
- `auto_update`: set value to ``datetime.utcnow()`` before inserts/saves;
only valid for datetime fields (default: ``False``)
"""
if not args:
raise TypeError('Field definition incorrect, please provide type')
elif not isinstance(args[0], type):
raise TypeError('Field input not a type')
self.data_type = args[0]
if ((self.data_type not in self.allowed_types and
not issubclass(self.data_type, self.allowed_types))):
raise TypeError('Field input type %s is not allowed' % self.data_type)
self.check_kwargs(kwargs, self.data_type)
# attributes
if 'auto_update' in kwargs and kwargs['auto_update']:
self.auto_update = self.data_type.utcnow # datetime.datetime
if 'document_class' in kwargs and kwargs['document_class']:
self.document_class = kwargs['document_class']
self.validator = self.generate_validator(self.data_type, **kwargs)
self.required = kwargs['required'] if 'required' in kwargs else True
if 'default' in kwargs:
self.default_value = kwargs['default']
if not callable(self.default_value):
validation_failed = False
try:
self.validator(self.default_value)
except ValidationError as e:
new_err = ('default value "%s"' % kwargs['default']) + ''.join(e.args)
validation_failed = True
if validation_failed:
raise TypeError(new_err)
# check if dict/list type and wrap copy in callable
if isinstance(self.default_value, (dict, list)):
def default_value_wrapper():
return copy.deepcopy(kwargs['default'])
self.default_value = default_value_wrapper
[docs] @classmethod
def check_kwargs(cls, kwargs, data_type):
"""Check keyword arguments & their values given to ``Field``
constructor such as ``default``, ``required`` ...
"""
err_str = data_type.__name__ + ': %s keyword argument not allowed or "%s" value invalid'
for k, v in kwargs.items():
# kwargs allowed for all data_types
if k in cls.allowed_kwargs:
if not cls.allowed_kwargs[k](v): # run kwarg validator
raise TypeError(err_str % (k, v))
# type specific kwargs
elif data_type in cls.extra_kwargs and k in cls.extra_kwargs[data_type]:
if not cls.extra_kwargs[data_type][k](v): # run kwarg validator
raise TypeError(err_str % (k, v))
else:
raise TypeError(err_str % (k, v))
[docs] def generate_validator(self, t, **kwargs):
"""Generates and returns validator function ``(value_to_check, field_name='')``.
``field_name`` kwarg is optional, used for better error reporting.
"""
def validator(val, field_name=''):
if val is None and 'required' in kwargs and not kwargs['required']:
return True
elif val is None:
raise ValidationError('%s: None is not allowed (field required)' % field_name)
if not isinstance(val, t):
raise ValidationError('%s: "%s" not an instance of %s but an instance of %s' %
(field_name, val, t, type(val)))
if isinstance(val, dict):
check_keys(val) # check against . & $ in keys
return True
return validator