Source code for flyforms.core

# coding=utf-8
from abc import ABCMeta, abstractmethod
from collections import Iterable, Callable

try:
    import simplejson as json
except ImportError:
    import json

from .common import UNSET
from .compat import with_metaclass, itervalues, iteritems
from .validators import validate_not_null, validate_required, TypeValidator, ValidationError


[docs]class UnboundForm(Exception): """ Raises when you try to serialize an :py:class:`.Form` bound with invalid data """ pass
[docs]class Field(with_metaclass(ABCMeta, object)): """ This is the base class for all builtin and user defined Fields. """ value_types = type # valid types of field value wrapper = None def __init__(self, required=True, null=False, validators=(), default=UNSET): """ :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 :raises TypeError: if passed arguments invalid """ # Define validators container self.base_validators = [] # Add not null validation, if necessary if null: self.base_validators.append(validate_not_null) self.null = null # Add required validation, if necessary if required: self.base_validators.append(validate_required) self.required = required # Check given default value if not isinstance(default, self.value_types) and default is not UNSET: raise TypeError( "Bad default value type. \nExpected {}, got {}".format(self.value_types, type(default)) ) self.default = default # Add type validation self.base_validators.append(TypeValidator(self.value_types)) # Clean given list of validators, if necessary if not isinstance(validators, Iterable): raise TypeError("Validators should be an iterable object") # Check is given validators are callable object and extend validators list if any(not isinstance(validator, Callable) for validator in validators): raise TypeError("Each validator should be a callable object") self.custom_validators = list(validators) @property def validators(self): """ .. versionchanged:: 1.0.0 Generator that returns :code:`base_validators` and :code:`custom_validators` items successively """ return (v for v in self.base_validators + self.custom_validators) @staticmethod
[docs] def field_validation_hook(method): """ Hook to avoid code duplicating in :py:meth:`Field.validate` method realizations that provides checking the value to :code:`None` and :py:data:`.UNSET` You may use it as decorator for :py:meth:`Field.validate` method in your custom fields """ def wrapper(field_obj, value): assert isinstance(field_obj, Field) # fixme debug if not field_obj.required and value is UNSET: # check is value set return if field_obj.null and value is None: # check for null return for validator in field_obj.validators: # run validators for full sequence validator(value) method(field_obj, value) return wrapper
@staticmethod
[docs] def field_binding_hook(method): """ Hook to avoid code duplicating in :py:meth:`Field.bind` method realizations that provides checking the value to :code:`None` and :py:data:`.UNSET` You may use it as decorator for :py:meth:`Field.bind` method in your custom fields """ def wrapper(field_obj, value): assert isinstance(field_obj, Field) # fixme debug if not field_obj.required and value is UNSET: # check is value set return field_obj.default, None if field_obj.null and value is None: # check is given value null return value, None return method(field_obj, value) return wrapper
[docs] def bind(self, value): """ .. versionadded:: 0.2.0 .. versionchanged:: 1.0.0 Validates given value via defined set of :code:`Validators`, wraps it into :py:attr:`.wrapper` and returns wrapped value and :code:`None` in second position. If some errors occurred returns an :py:data:`.UNSET` and this errors 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 :returns: bound value and occurred errors (if there were no errors - :code:`None` will be returned in second position) """ if not self.required and value is UNSET: # check is value set return self.default, None if self.null and value is None: # check is given value null return value, None try: error = None for validator in self.validators: # Run all validators validator(value) if self.wrapper is not None: # Wrap value value = self.wrapper(value) except ValidationError as e: value = UNSET error = e.message return value, error
@abstractmethod
[docs] def validate(self, value): """ Validates given value via defined set of :code:`Validators` """
[docs]class UnboundField(object): """ Field without value """ __slots__ = ("name", "field", "errors") def __init__(self, name, field_type, errors): """ :param name: field name :param field_type: field class :param errors: field errors """ self.name = name self.field = field_type self.errors = errors def __repr__(self): return "<Unbound{}({}, errors: {})>".format(self.field, self.name, self.errors)
class DefaultMetaOptions(object): skip_extra = False unbound_field_render = UnboundField
[docs]class FormMetaOptions(object): """ Provides customization of :py:class:`.Form` instances behaviour. """ def __init__(self, **kwargs): """ .. py:attribute:: skip_extra (default: :code:`False`) if not defined, all extra field will be interpreted as errors during Form instantiation .. py:attribute:: unbound_field_render (default: :py:class:`.UnboundField`) all unbound Form fields will be replaced with instances of this class """ self.skip_extra = kwargs.pop("skip_extra", DefaultMetaOptions.skip_extra) self.unbound_field_render = kwargs.pop("unbound_field_render", DefaultMetaOptions.unbound_field_render) self.kwargs = kwargs
[docs]class FormMeta(type): """ The metaclass for Form and it's subclasses. It`s main responsibility - find all declared fields in the form and it`s bases. It also replaces the declared fields with :py:class:`.FormField` descriptor. """ def __new__(mcs, name, bases, dct): # Parse meta options from defined Meta meta_opt_cls = dct.pop("Meta", DefaultMetaOptions) meta_opt = {k_opt: getattr(meta_opt_cls, k_opt) for k_opt in dir(meta_opt_cls) if not k_opt.startswith("_")} fields = [] # create a container for Form's fields names for attr, val in iteritems(dct): # walk through class attributes if isinstance(val, Field): # find all Field instances fields.append(attr) # catch them dct[attr] = FormField(attr, val) # and replace with descriptor # Update class attributes dct["_fields"] = set(fields) dct["_meta"] = FormMetaOptions(**meta_opt) cls = super(FormMeta, mcs).__new__(mcs, name, bases, dct) # create new class # Get all fields names from MRO _fields = set() # prepare container for Form fields names for base in cls.__mro__: # walk through the MRO _fields |= getattr(base, "_fields", set()) # get fields names from each base cls._fields = _fields # update Form's fields return cls # noinspection PyProtectedMember
[docs]class FormField(object): """ The descriptor for fields in :py:class:`.Form`. It`s behavior depends on whether the form is instantiated or not. """ def __init__(self, name, field_obj): """ :param name: declared field class attribute name :param field_obj: instance of declared field """ if not isinstance(field_obj, Field): # fixme debug raise TypeError("You should bind FormField with Field subclass instance, not {}".format(type(field_obj))) self.name = name self.field = field_obj
[docs] def __get__(self, instance, owner): """ If form is instantiated returns bound data, otherwise - instance of declared field """ if not issubclass(owner, Form): # fixme debug AttributeError("You can\'t use FormField without Form") if instance is None: return self.field return instance._raw_data.get(self.name, self.field.default)
[docs] def __set__(self, instance, value): """ Calls :py:meth:`.Field.bind` method and puts the result to :py:attr:`.Form._raw_data`. If :py:meth:`.Field.bind` returns :py:data:`.UNSET` value or there are errors (second return value is not :code:`None`) an instance of :py:class:`.UnboundField` will be put into :py:attr:`.Form._raw_data`. If form is instantiated :code:`AttributeError` will be raised. """ if not isinstance(instance, Form): # fixme debug raise AttributeError("You can\'t use FormField without Form") if self.name in instance._raw_data: raise AttributeError("You can\'t overwrite already bound field {}!".format(self.name)) bound_value, errors = self.field.bind(value) if errors is not None: instance.errors[self.name] = errors if bound_value is UNSET: bound_value = instance._meta.unbound_field_render(self.name, self.field.__class__.__name__, errors) instance._raw_data[self.name] = bound_value
def __delete__(self, instance): raise AttributeError("You can\'t delete Form fields!") # noinspection PyProtectedMember
[docs]class Form(with_metaclass(FormMeta, object)): """ The root class for all Forms """ _fields = set() _meta = FormMetaOptions() def __init__(self, **data): """ :param data: additional data to form :type data: dict When a Form is instantiated you can access given data via instance attributes or get everything at once using :py:meth:`.to_python()` method """ self._raw_data = {} # create a container for Form data self.errors = {} # create a container for Form's errors for field_name in self._fields: # walk through class fields setattr(self, field_name, data.pop(field_name, UNSET)) # and try to set up them if not self._meta.skip_extra: for unk in data: # if some extra fields given it's an error self.errors[unk] = "Unknown field %s in data for Form %s" % (unk, self.__class__.__name__) @property def is_valid(self): """ Checks is Form instance valid. Returns True if there are no errors. Otherwise, False. """ return self.errors == {} @property def is_bound(self): """ Checks is Form instance bound. Returns True if there are no :py:class:`.UnboundField` instances in :py:attr:`._raw_data`. Otherwise, False. """ return not any(isinstance(v, UnboundField) for v in itervalues(self._raw_data))
[docs] def to_python(self): """ .. versionchanged:: 1.0.0 .. versionadded:: 0.3.0 Get form data as a :code:`dict` :returns: dictionary that contains bound data of valid form :raise UnboundForm: if :py:attr:`is_valid` returns False """ if not self.is_valid: raise UnboundForm("Form bound with invalid data") return {f: self._raw_data[f] for f in self._fields if not isinstance(self._raw_data[f], self._meta.unbound_field_render)}
@classmethod
[docs] def validate(cls, **schema): """ Class method provides data validation without :py:class:`.Form` instantiation by calling :py:meth:`.Field.validate` method of all declared fields :param schema: data to validate :return: boolean flag is data valid for this :py:class:`.Form` """ d = dict(schema) for field_name in cls._fields: field_obj = getattr(cls, field_name) try: field_obj.validate(d.pop(field_name, UNSET)) except ValidationError: return False if (not d) or cls._meta.skip_extra: return True return False