# coding=utf-8
"""
This module contains a collection of fields classes
"""
import warnings
from abc import ABCMeta
from collections import Iterable
from itertools import chain
from datetime import datetime
from flyforms.common import is_set, jsonify_types, UNSET, FrozenDict
from flyforms.validators import *
from flyforms.validators import Validator
from flyforms.compatibility import string_types, with_metaclass, NoneType
__all__ = (
"StringField",
"EmailField",
"IntField",
"FloatField",
"BooleanField",
"Ip4Field",
"ListField",
"ArrayField",
"DatetimeField",
"DictField"
)
[docs]class Field(with_metaclass(ABCMeta, object)):
"""
This the base class for all Fields.
Fields instances reflect and validate data.
"""
value_types = (type,) # valid types of field value
def __init__(self, required=True, choices=(), validators=(), **kwargs):
"""
:param required: boolean flag is this field required or can be empty
:type required: bool
:param choices: iterable object contains possible values of this field
:type choices: iterable
:param validators: the additional validators for field
:type validators: list of callable
:param default: the default value of the field
:type default: instance of value_types
:raises TypeError: if passed arguments invalid
"""
# Define validators container
self.base_validators = []
# Add required validation, if necessary
if required:
self.base_validators.append(RequiredValidator())
self.required = required
# Check given default value
self.default = kwargs.get("default", UNSET)
if not isinstance(self.default, self.value_types) and self.default is not UNSET:
raise TypeError(
"Bad default value type. \nExpected {}, got {}".format(self.value_types, type(self.default))
)
if required and self.default is not UNSET:
warnings.warn(
"Warning in field %s: default value given for required field. I'll be skipped.", RuntimeWarning
)
self.default = UNSET
# Add type validation
self.base_validators.append(TypedValidator(self.value_types))
# Check is given choices iterable object
if not isinstance(choices, Iterable):
raise TypeError("Choices should be an iterable object")
# Check choices type
for choice in choices:
if not isinstance(choice, self.value_types):
raise TypeError(
"Bad value type in choices. \nExpected %s, got %s" % (
self.value_types,
type(choice)
)
)
# Add choices validation, if necessary
if choices:
self.base_validators.append(EntryValidator(choices))
# Clean given list of validators, if necessary
if not isinstance(validators, Iterable):
validators = (validators,)
# Check is given validators are callable object and extend validators list
for validator in validators:
if not isinstance(validator, Validator):
raise TypeError("Validator should be a child of Validator superclass")
self.custom_validators = validators
[docs] def validate(self, value):
"""
Validates given value via defined set of :code:`Validators`
:param value: the value to validate
"""
warnings.simplefilter('always', DeprecationWarning)
warnings.warn("This method deprecated and will be removed in v1.0.0")
warnings.simplefilter('ignore', DeprecationWarning)
# Check is value set
if not self.required and not is_set(value):
return
# Run all validators
for validator in self.validators:
validator(value)
[docs] def is_valid(self, value):
"""
The 'silent' variant of value validation.
:param value: the value to validate
:return: True if given value is valid, otherwise - False
"""
warnings.simplefilter('always', DeprecationWarning)
warnings.warn("This method deprecated and will be removed in v1.0.0")
warnings.simplefilter('ignore', DeprecationWarning)
# Check is value set
if not self.required and not is_set(value):
return True
# Check value via validators
for validator in self.validators:
if not validator.is_valid(value):
return False
return True
@property
def validators(self):
"""
Chain, contains :code:`base_validators` and :code:`custom_validators`
"""
return chain(self.base_validators, self.custom_validators)
[docs] def bind(self, value):
"""
Validates and given value via defined set of :code:`Validators` and returns it
If value is mutable obj (for example :code:`list`) it'll be converted to immutable (for example :code:`tuple`)
:param value: the value to bind
:param name: field name (optional)
:type name: str
:return: given value or :code:`field.default` if value is :py:data:`.UNSET`
"""
# Check is value set
if not self.required and not is_set(value):
return self.default
# Run all validators
for validator in self.validators:
validator(value)
return value
[docs]class StringField(Field):
"""
Reflects Python strings
"""
value_types = string_types # valid types of field value
def __init__(self, min_length=None, max_length=None, regex="", **kwargs):
"""
:param required: boolean flag is this field required
:type required: bool
:param min_length: the minimum length of the string
:type min_length: int or None
:param max_length: the maximum length of the string
:type max_length: int or None
:param regex: the regular expression to validate
:type regex: str or regexp
:param choices: iterable object contains possible values of this field
:type choices: iterable
:param validators: the additional validators for field
:type validators: list of callable
:param default: the default value of the field
:type default: instance of value_types
"""
super(StringField, self).__init__(**kwargs)
if min_length:
self.base_validators.append(MinLengthValidator(min_length, False))
if max_length:
self.base_validators.append(MaxLengthValidator(max_length, False))
if regex:
self.base_validators.append(RegexValidator(regex))
[docs]class EmailField(Field):
"""
Reflects Python string corresponding to an email
"""
value_types = string_types # valid types of field value
def __init__(self, **kwargs):
"""
:param required: boolean flag is this field required
:type required: bool
:param choices: iterable object contains possible values of this field
:type choices: iterable
:param validators: the additional validators for field
:type validators: list of callable
:param default: the default value of the field
:type default: instance of value_types
"""
super(EmailField, self).__init__(**kwargs)
self.base_validators.append(EmailValidator())
[docs]class IntField(Field):
"""
Reflects Python integer (:code:`int`)
"""
value_types = (int, NoneType) # valid types of field value
def __init__(self, min_value=None, max_value=None, **kwargs):
"""
:param required: boolean flag is this field required
:type required: bool
:param min_value: the minimum valid value
:param max_value: the maximum valid value
:param choices: iterable object contains possible values of this field
:type choices: iterable
:param validators: the additional validators for field
:type validators: list of callable
:param default: the default value of the field
:type default: instance of value_types
"""
super(IntField, self).__init__(**kwargs)
if min_value is not None:
self.base_validators.append(MinValueValidator(min_value, False))
if max_value is not None:
self.base_validators.append(MaxValueValidator(max_value, False))
[docs]class FloatField(IntField):
"""
Reflects Python float (:code:`float`)
"""
value_types = (float, NoneType) # valid types of field value
[docs]class BooleanField(Field):
"""
Reflects Python boolean (:code:`bool`)
"""
value_types = (bool, NoneType) # valid types of field value
def __init__(self, **kwargs):
"""
:param required: boolean flag is this field required
:type required: bool
:param validators: the additional validators for field
:type validators: list of callable
:param default: the default value of the field
:type default: instance of value_types
"""
super(BooleanField, self).__init__(**kwargs)
[docs]class Ip4Field(Field):
"""
Reflects Python string corresponding to an IPv4 address
"""
value_types = string_types # valid types of field value
def __init__(self, **kwargs):
"""
:param required: boolean flag is this field required
:type required: bool
:param choices: iterable object contains possible values of this field
:type choices: iterable
:param validators: the additional validators for field
:type validators: list of callable
:param default: the default value of the field
:type default: instance of value_types
"""
super(Ip4Field, self).__init__(**kwargs)
self.base_validators.append(Ip4AddressValidator())
[docs]class ListField(Field):
"""
Reflects iterable Python objects
"""
value_types = Iterable
def __init__(self, min_length=None, max_length=None, jsonify=True, **kwargs):
"""
:param required: boolean flag is this field required or can be empty
:type required: bool
:param min_length: minimum iterable length
:type min_length: int
:param max_length: maximum iterable length
:type max_length: int
:param jsonify: if passed :code:`item_type` should be one of :py:data:`.jsonify_types`
:type jsonify: bool
:param validators: the additional validators for field
:type validators: list of callable
:param default: the default value of the field
:type default: instance of value_types
.. versionadded:: 0.2.0
"""
super(ListField, self).__init__(**kwargs)
if min_length:
self.base_validators.append(MinLengthValidator(min_length, False))
if max_length:
self.base_validators.append(MaxLengthValidator(max_length, False))
if jsonify:
self.base_validators.append(JsonItemTypedValidator())
def bind(self, value):
value = super(ListField, self).bind(value)
return tuple(value)
[docs]class ArrayField(ListField):
"""
.. versionadded:: 0.2.0
Reflects iterable objects where each item same type
"""
def __init__(self, item_type, jsonify=True, **kwargs):
"""
:param item_type: type of each item in the list
:param min_length: minimum iterable length
:type min_length: int
:param max_length: maximum iterable length
:type max_length: int
:param jsonify: if passed :code:`item_type` should be one of :py:data:`.jsonify_types`
:type jsonify: bool
:param required: boolean flag is this field required or can be empty
:type required: bool
:param validators: the additional validators for field
:type validators: list of callable
:param default: the default value of the field
:type default: instance of value_types
"""
if jsonify and item_type not in jsonify_types:
raise TypeError("Type %s is not supported for JSON encoding and decoding operations" % item_type)
super(ArrayField, self).__init__(jsonify=jsonify, **kwargs)
self.base_validators.append(ItemTypedValidator(item_type))
[docs]class DatetimeField(StringField):
"""
.. versionadded:: 0.3.0
A datetime field.
Parse string contains datetime via datetime.strptime
"""
def __init__(self, required=True, fmt="%Y-%m-%d %H:%M:%S", now=False):
"""
:param fmt: datetime format
:type fmt: str
:param now: if passed the default value will be datetime.now()
:type now: bool
.. versionadded:: 0.3.0
"""
super(DatetimeField, self).__init__(required=required)
self.fmt = fmt
if now:
self.default = datetime.now().strftime(self.fmt)
def bind(self, value):
"""
Validates given datetime and wraps it into :py:class:`datetime.datetime`
"""
try:
return datetime.strptime(super(DatetimeField, self).bind(value), self.fmt)
except ValueError as error:
raise ValidationError(str(error))
[docs]class DictField(Field):
"""
.. versionadded:: 0.3.0
Reflects Python dict
"""
value_types = dict
def __init__(self, schema, **kwargs):
"""
:param schema: template to dict validation
:type schema: dict
:param required: boolean flag is this field required or can be empty
:type required: bool
:param validators: the additional validators for field
:type validators: list of callable
:param default: the default value of the field
:type default: instance of value_types
"""
if not isinstance(schema, dict):
raise TypeError("Schema for DictField should be dict instance")
for key, value in schema.items():
if not isinstance(value, Field):
raise TypeError("Bad field %s in schema. Expected Field subclass instance, got %s" % type(value))
super(DictField, self).__init__(**kwargs)
self.schema = schema
def bind(self, value):
"""
Validates given dict and wraps it into :py:class:`.FrozenDict`
"""
d = super(DictField, self).bind(value)
assert isinstance(d, dict)
if d is not self.default:
for key, value in d.items():
if key not in self.schema:
raise ValidationError("Unknown key }".format(key))
d[key] = self.schema[key].bind(value)
return FrozenDict(**d)