# coding=utf-8
"""
This module contains a collection of fields classes
"""
from abc import ABCMeta
from collections import Iterable
from itertools import chain
from warnings import warn
from flyforms.common import is_set, jsonify_types, UNSET
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"
)
[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
[docs] 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:
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
"""
# 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
"""
# 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
: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
[docs] 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
[docs] 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
[docs] 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
[docs] 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
[docs] 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
[docs] def __init__(self, min_length=None, max_length=None, jsonify=True, **kwargs):
"""
:param min_length: minimum iterable length
:param max_length: maximum iterable length
:param jsonify: if passed :code:`item_type` should be one of :py:data:`.jsonify_types`
:param kwargs: basic :py:class:`.Field` parameters
"""
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):
"""
Reflects iterable objects where each item same type
"""
[docs] def __init__(self, item_type, jsonify=True, **kwargs):
"""
:param item_type: type of each item in the list
:param kwargs: basic :py:class:`.ListField` parameters
"""
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))