# coding=utf-8
"""
This module contains a collection of fields classes
"""
from collections import Iterable, Callable
from datetime import datetime
from .common import jsonify_types, FrozenDict, UNSET
from .core import Form, Field
from .validators import *
from .compat import string_base, text_type
__all__ = (
"StringField",
"EmailField",
"IntField",
"FloatField",
"BooleanField",
"Ip4Field",
"ListField",
"ArrayField",
"DatetimeField",
"DictField",
"EmbeddedFormField"
)
[docs]class SelectField(Field):
"""
.. versionadded:: 1.0.0
This is the base class for all Fields that reflect single values such as :code:`int`, :code:`float`, :code:`bool`
and etc. It provides to specify list of valid values (choices). Any passed values which are not in the defined
choices will cause validation fails.
"""
def __init__(self, choices=(), **kwargs):
"""
:param required: boolean flag is this field required or may be :py:data:`.UNSET`
:type required: bool
:param null: boolean flag is field value may be :code:`None`
:type null: 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
:param choices: iterable object contains possible values of this field
:type choices: Iterable
"""
super(SelectField, self).__init__(**kwargs)
# Add choices validation, if necessary
if choices:
self.base_validators.append(EntryValidator(choices, self.value_types))
@Field.field_validation_hook
def validate(self, value):
pass
[docs]class StringField(SelectField):
"""
Reflects Python strings
"""
value_types = string_base # valid types of field value
wrapper = text_type
def __init__(self, min_length=None, max_length=None, regex="", **kwargs):
"""
:param required: boolean flag is this field required
:type required: bool
:param null: boolean flag is field value may be :code:`None`
:type null: 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(StringField):
"""
Reflects Python string corresponding to an email
"""
def __init__(self, **kwargs):
"""
:param required: boolean flag is this field required
:type required: bool
:param null: boolean flag is field value may be :code:`None`
:type null: 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(SelectField):
"""
Reflects Python :code:`int` values
"""
value_types = int # 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 null: boolean flag is field value may be :code:`None`
:type null: 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 :code:`float` values
"""
value_types = float # valid types of field value
[docs]class BooleanField(SelectField):
"""
Reflects Python :code:`bool` values
"""
value_types = bool # valid types of field value
def __init__(self, **kwargs):
"""
:param required: boolean flag is this field value may be :py:data:`.UNSET`
:type required: bool
:param null: boolean flag is field value may be :code:`None`
:type null: 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(StringField):
"""
Reflects Python string corresponding to an IPv4 address
"""
def __init__(self, **kwargs):
"""
:param required: boolean flag is this field required
:type required: bool
:param null: boolean flag is field value may be :code:`None`
:type null: 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 SequenceField(Field):
"""
.. versionadded:: 1.0.0
This is the base class for all Fields that reflect iterable values such as list, tuple and etc.
It provides to specify such as minimum and maximum possible iterable length and validators for each item.
"""
value_types = Iterable
wrapper = tuple
def __init__(self, min_length=None, max_length=None, item_validators=(), **kwargs):
"""
:param required: boolean flag is this field required or may be :py:data:`.UNSET`
:type required: bool
:param null: boolean flag is field value may be :code:`None`
:type null: 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
:param min_length: minimum iterable length
:type min_length: int
:param max_length: maximum iterable length
:type max_length: int
:param item_validators: the additional validators for every item
:type item_validators: list of Callable
"""
super(SequenceField, self).__init__(**kwargs)
# Check is given item validators are callable object and extend validators list
if any(not isinstance(validator, Callable) for validator in item_validators):
raise TypeError("Each validator should be a callable object")
self.item_validators = [validate_required] + list(item_validators)
if min_length:
self.base_validators.append(MinLengthValidator(min_length, False))
if max_length:
self.base_validators.append(MaxLengthValidator(max_length, False))
@Field.field_validation_hook
def validate(self, value):
for item in value: # run item validators
for item_validator in self.item_validators:
item_validator(item)
[docs]class ListField(SequenceField):
"""
Reflects iterable Python objects
"""
def __init__(self, jsonify=True, **kwargs):
"""
:param required: boolean flag is this field required or may be :py:data:`.UNSET`
:type required: bool
:param null: boolean flag is field value may be :code:`None`
:type null: bool
:param min_length: minimum iterable length
:type min_length: int
:param max_length: maximum iterable length
:type max_length: int
:param item_validators: the additional validators for every item
:type item_validators: list of Callable
:param jsonify: if True all items 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
"""
super(ListField, self).__init__(**kwargs)
if jsonify:
self.item_validators.append(TypeValidator(jsonify_types))
[docs]class ArrayField(SequenceField):
"""
.. 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 required: boolean flag is this field required or may be :py:data:`.UNSET`
:type required: bool
:param null: boolean flag is field value may be :code:`None`
:type null: bool
:param min_length: minimum iterable length
:type min_length: int
:param max_length: maximum iterable length
:type max_length: int
:param item_validators: the additional validators for every item
:type item_validators: list of Callable
:param jsonify: if True all items 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
"""
if jsonify and not any(issubclass(item_type, jsonify_type) for jsonify_type in jsonify_types):
raise TypeError("Type %s is not supported for JSON encoding and decoding operations" % item_type)
item_validators = kwargs.pop("item_validators", [])
item_validators.append(TypeValidator(item_type))
super(ArrayField, self).__init__(item_validators=item_validators, **kwargs)
[docs]class DatetimeField(Field):
"""
.. versionadded:: 0.3.0
A datetime field.
Parse string contains datetime via datetime.strptime
"""
value_types = string_base
wrapper = text_type
def __init__(self, required=True, fmt="%Y-%m-%d %H:%M:%S", now=False, null=False, validators=()):
"""
:param required: boolean flag is this field value may be :py:data:`.UNSET`
:type required: bool
:param fmt: datetime format
:type fmt: str
:param now: if passed the default value will be datetime.now()
:type now: bool
:param null: boolean flag is field value may be :code:`None`
:type null: bool
:param validators: the additional validators for field
:type validators: list of callable
"""
default = datetime.now().strftime(self.fmt) if now else UNSET
super(DatetimeField, self).__init__(required=required, null=null, validators=validators, default=default)
self.fmt = fmt
@Field.field_validation_hook
def validate(self, value):
try:
datetime.strptime(value, self.fmt)
except ValueError as error:
raise ValidationError(error.message)
[docs]class DictField(Field):
"""
.. versionadded:: 0.3.0
Reflects Python dict
"""
value_types = dict
wrapper = FrozenDict
def __init__(self, schema, **kwargs):
"""
:param schema: template to dict validation
:type schema: dict
:param required: boolean flag is this field value may be :py:data:`.UNSET`
:type required: bool
:param null: boolean flag is field value may be :code:`None`
:type null: 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" % (
key, type(value)
)
)
super(DictField, self).__init__(**kwargs)
self.schema = schema
@Field.field_validation_hook
def validate(self, value):
d = dict(value)
for key, field_obj in self.schema:
field_obj.validate(d.pop(key, UNSET))
if d != {}:
raise ValidationError("Unknown fields: %s" % d.keys())
@Field.field_binding_hook
def bind(self, value):
try:
for validator in self.validators: # run validators for whole dict
validator(value)
except ValidationError as e: # if ValidationError occurred
bound = UNSET
error = e.message
else:
bound = {}
error = {}
d = dict(value)
for key, field_obj in self.schema.items():
bv, er = field_obj.bind(d.pop(key, UNSET))
if er is None:
if bv is not UNSET:
bound[key] = bv
continue
error[key] = er
for unk in d:
error[unk] = "Unknown field: %s" % unk
bound = FrozenDict(bound)
error = None if error == {} else error
return bound, error