from collections import OrderedDict
from .. import cache
from .. import exceptions
from .. import schematics
[docs]class UnsetValue(object):
"""This class exists to put a label on the type of value that represents
when a field does not _yet_ have a value.
"""
pass
Unset = UnsetValue()
class TypeMeta(type):
def __new__(mcs, name, bases, namespace):
# attribute accumulators
validate_functions = OrderedDict()
# gather values from base classes
for b in reversed(bases):
if hasattr(b, '_validate_functions'):
validate_functions.update(b._validate_functions)
# gather typerighter attributes
for k, v in namespace.items():
if k.startswith('validate_') and callable(v):
validate_functions[k] = v
# attach collected values
namespace['_validate_functions'] = validate_functions
# create the new type
type_class = type.__new__(mcs, name, bases, namespace)
# create schematic for type
schematic = schematics.Schematic(type_class)
setattr(type_class, '_schematic', schematic)
# put type in cache
cache.TypeCache().add(type_class)
return type_class
[docs]def skip_falsy(method):
"""A decorator that intercepts method calls to prevent falsy inputs from
being validated unnecessarily.
"""
def wrapper(self, value, *a, **kw):
if self.is_falsy(value):
return value
return method(self, value, *a, **kw)
return wrapper
[docs]class Type(object, metaclass=TypeMeta):
"""This class represents the top, and thus most ambiguous, point of the
Typerighter hierarchy.
It's purpose is to define the baseline expectations for every other `Type`
in this library.
"""
NATIVE = object # identity function
def __init__(self, default=Unset, required=False, strict=False):
"""Base initializer for Types. Implements the basic features for Types
and provides the Type config options, such as setting a default value.
:param object default: Any value you want to be used as a default
inside `to_primitive` and `to_native`.
:param bool required: If `True`, `Unset` values will trigger an
exception during validation
:param bool strict: Enforces all values are exactly the right type by
validating without type coercion.
"""
self.required = required
self.strict = strict
self.default = default
if default != Unset:
self.to_primitive = self._apply_default(self.to_primitive)
self.to_native = self._apply_default(self.to_native)
[docs] def is_falsy(self, value):
"""Checks a value and responds saying whether the `Type` considers it
falsy.
:param object value: The value to inspect
:return: True or False
"""
if value == Unset or value is None:
return True
return False
[docs] def is_coercible(self, value):
"""Checks a value for whether or not it can be converted to the correct
type. Falls back to the stricter `is_type_match` if `self.strict` is
True.
:param object value: The value to inspect
"""
if self.strict:
return self.is_type_match(value)
try:
self.to_native(value)
return True
except Exception:
return False
[docs] def is_type_match(self, value):
"""Checks if a value is an instance of this Type's native type.
:param object value: The value to inspect
"""
return isinstance(value, self.NATIVE)
[docs] def to_schematic(self):
"""Returns a Type's Schematic
"""
return (self.__class__.__name__, self._schematic.init_args)
def _apply_default(self, method):
def wrapper(value, *a, **kw):
if value == Unset and self.default != Unset:
return self.default
return method(value, *a, **kw)
return wrapper
[docs] def to_primitive(self, value):
"""Converts a value to the primitive form of this type
:param object value: The value to convert
"""
return value
[docs] def to_native(self, value):
"""Converts a value to the native form of this type
:param object value: The value to convert
"""
if self.is_falsy(value):
return value
if self.NATIVE != object:
return self.NATIVE(value)
return value
[docs] def validate(self, value):
"""This validation function is the primary function responsible for
calling all associated validators and for managing any details
related to aggregation of validation results.
:param object value: The value to convert
"""
self._validate_required(value)
self._validate_type_match(value)
native = self.to_native(value)
for func in self._validate_functions.values():
func(self, native)
def _validate_required(self, value):
if self.required and value == Unset:
e_msg = "Value required but not found"
raise exceptions.ValidationException(e_msg)
@skip_falsy
def _validate_type_match(self, value):
if self.strict and self.is_type_match(value):
return
elif self.is_coercible(value):
return
e_msg = "Value doesnt match type format {}".format(value)
raise exceptions.ValidationException(e_msg)
def __setattr__(self, name, value):
"""This method makes it impossible to overwrite any attributes
that are subclasses of `Type`.
"""
propobj = getattr(self.__class__, name, None)
if propobj and isinstance(propobj, Type):
return
super().__setattr__(name, value)