Source code for springfield.fields

from datetime import datetime
import unicodedata
from anticipate.adapt import adapt, AdaptError
from springfield.timeutil import date_parse, generate_rfc3339
from springfield.types import Empty
from decimal import Decimal
import re

[docs]class FieldDescriptor(object): """ A descriptor that handles setting and getting :class:`Field` values on an :class:`Entity`. """ def __init__(self, name, field): """ :param name: The name of the :class:`Entity`'s attribute :param field: A :class:`Field` instance """ self.name = name self.field = field self.__doc__ = self.field.__doc__ def __get__(self, instance, owner): """ Get the :class:`Field`'s value. If the :class:`Field`'s default is a callable, it will be called and adapted if needed. """ if instance is None: return self return self.field.get(instance, self.name) def __set__(self, instance, value): """ Set a value for this :class:`Field`. The value is adapted to the :class:`Field`'s type if needed. """ old_value = instance.__values__.get(self.name) new_value = self.field.set(instance, self.name, value) if new_value != old_value: instance.__changes__.add(self.name)
[docs]class Field(object): """ A field """ def __init__(self, default=Empty, doc=None, *args, **kwargs): """ :param default: The default value if no value is assigned to this field :param doc: The docstring to assign to this field and its descriptor """ if callable(default): self.default = default elif default is not None and default is not Empty: self.default = self.adapt(default) else: self.default = default self.__doc__ = doc
[docs] def init(self, cls): """ Initialize the field for its owner :class:`Entity` class. Any specialization that needs to be done based on the :class:`Entity` class itself should be done here. :param cls: An :class:`Entity` class. """
def get(self, instance, name): # Get value from document instance if available, if not use default value = instance.__values__.get(name) if value is Empty: value = self.default # Allow callable default values if callable(value): value = self.adapt(value()) if value is Empty: value = None return value def set(self, instance, name, value): if value is Empty: if name in instance.__values__: del instance.__values__[name] else: instance.__values__[name] = self.adapt(value) return instance.__values__[name]
[docs] def adapt(self, value): """ Convert the value from the input type to the expected type if needed. :returns: The adapted value """ return value
[docs] def make_descriptor(self, name): """ Create a descriptor for this :class:`Field` to attach to an :class:`Entity`. """ return FieldDescriptor(name=name, field=self)
[docs] def flatten(self, value): """ Get the value as a basic Python type :param value: An :class:`Entity`'s value for this :class:`Field` """ return value
[docs] def jsonify(self, value): """ Get the value as a suitable JSON type :param value: An :class:`Entity`'s value for this :class:`Field` """ return value
[docs]class AdaptableTypeField(Field): """ A :class:`Field` that has a specific type and can be adapted to another type. """ __adapters__ = None #: The value type this :class:`Field` expects type = None
[docs] def adapt(self, value): """ Convert the `value` to the `self.type` for this :class:`Field` """ if value is None or value is Empty: return value if isinstance(value, self.type): return value elif hasattr(value, '__adapt__'): # Use an object's own adapter to adapt. try: return value.__adapt__(self.type) except TypeError as e: pass if hasattr(self.type, '__adapt__'): # Try using the type's adapter return self.type.__adapt__(value) if self.__class__.__adapters__: # Use a registered adapter adapter = self.__class__.__adapters__.get(type(value), None) if adapter: return adapter(value) try: # Use generic adapters return adapt(value, self.type) except AdaptError: pass raise TypeError('Could not adapt %r to %r' % (value, self.type))
@classmethod
[docs] def register_adapter(cls, from_cls, func): """ Register a function that can handle adapting from `from_cls` for this field. TODO This may be a bad idea, re-evaluate how to register adapters. """ if not cls.__adapters__: cls.__adapters__ = {} cls.__adapters__[from_cls] = func
[docs]class IntField(AdaptableTypeField): """ A :class:`Field` that contains an `int`. """ type = int
[docs] def adapt(self, value): """ Adapt `value` to an `int`. :param value: Can be an `int`, `float`, `long`, or a `str` or `unicode` that looks like an `int`. `float` or `long` values must represent an integer, i.e. no decimal places. """ try: return super(IntField, self).adapt(value) except TypeError: if isinstance(value, basestring): return int(value) elif isinstance(value, (float, long)): t = int(value) if t == value: return t raise
[docs]class FloatField(AdaptableTypeField): """ A :class:`Field` that contains a `float`. """ type = float
[docs] def adapt(self, value): """ Adapt `value` to a `float`. :param value: Can be an `int`, `float`, `long`, or a `str` or `unicode` that looks like a `float`. `long` values will remain `long`s. """ try: return super(FloatField, self).adapt(value) except TypeError: if isinstance(value, (basestring, int)): return float(value) elif isinstance(value, (float, long)): return value elif isinstance(value, Decimal): return float(value) raise
[docs]class BooleanField(AdaptableTypeField): """ A :class:`Field` that contains a `bool`. """ type = bool
[docs] def adapt(self, value): """ Adapt `value` to a `bool`. :param value: A boolean-like value. A `float`, `int`, or `long` will be converted to: * `True` if equal to `1` * `False` if equal to `0` String values will be converted to (case-insensitive): * `True` if equal to "yes", "true", "1", or "on" * `False` if equal to "no", "false", "0", or "off" """ try: return super(BooleanField, self).adapt(value) except TypeError: if isinstance(value, basestring): str = value.lower() if str in ['yes', 'true', '1', 'on']: return True elif str in ['no', 'false', '0', 'off']: return False elif isinstance(value, (float, long, int)): if value == 1: return True elif value == 0: return False raise
[docs]class StringField(AdaptableTypeField): """ A :class:`Field` that contains a unicode string. """ type = unicode
[docs] def adapt(self, value): """ Adapt `value` to `unicode`. """ try: return super(StringField, self).adapt(value) except TypeError: if isinstance(value, basestring): return unicode(value) raise
[docs]class SlugField(StringField): """ :class:`Field` whose value is a slugified string. A slug is a string converted to lowercase with whitespace replace with a "-" and non-ascii chars converted to their ascii equivalents. """
[docs] def adapt(self, value): """ Adapt `value` to a slugified string. :param value: Any string-like value """ # Make sure it's a unicode first value = super(SlugField, self).adapt(value) if not value: return '' slug = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') slug = re.sub(r'[^\w\s-]', '', slug).strip().lower() slug = re.sub(r'[-\s]+', '-', slug).strip('-') return slug
[docs]class DateTimeField(AdaptableTypeField): """ :class:`Field` whose value is a Python `datetime.datetime` """ type = datetime
[docs] def adapt(self, value): """ Adapt `value` to a `datetime.datetime` instance. :param value: A date-like value. RFC3339 formatted date-strings are supported. If `dateutil` is installed, `dateutil.parser.parse` is used which supports many date formats. """ try: return super(DateTimeField, self).adapt(value) except TypeError: if isinstance(value, basestring): return date_parse(value) raise
[docs] def jsonify(self, value): """ Get the date as a RFC3339 date-string """ if value is not None: return generate_rfc3339(value)
[docs]class EmailField(StringField): """ :class:`Field` with an email value """
[docs]class UrlField(StringField): """ :class:`Field` with a URL value """
[docs]class EntityField(AdaptableTypeField): """ :class:`Field` that can contain an :class:`Entity` """ def __init__(self, entity, *args, **kwargs): """ :param entity: The :class:`Entity` class to expect for this field. Use 'self' to use the :class:`Entity` class that this field is already bound to. """ self.type = entity super(EntityField, self).__init__(*args, **kwargs) def init(self, cls): if self.type == 'self': self.type = cls
[docs] def flatten(self, value): """ Convert an :class:`Entity` to a `dict` containing native Python types. """ if value is not None: return value.flatten()
[docs] def jsonify(self, value): """ Convert an :class:`Entity` into a JSON object """ if value is not None: return value.jsonify()
[docs]class IdField(Field): """ A :class:`Field` that is used as the primary identifier for an :class:`Entity` TODO This should accept another Field type to contain the ID """
[docs]class CollectionField(Field): """ A :class:`Field` that can contain an ordered list of values matching a specific :class:`Field` type. """ #: The :class:`Field` this collection contains field = None def __init__(self, field, *args, **kwargs): if not isinstance(field, Field): field = field() self.field = field super(CollectionField, self).__init__(*args, **kwargs) def init(self, cls): self.field.init(cls)
[docs] def adapt(self, value): """ Adapt all values of an iterable to the :class:`CollectionField`'s field type. """ if value is not None: values = [] for item in value: values.append(self.field.adapt(item)) return values
[docs] def flatten(self, value): """ Convert all values of an iterable to the :class:`CollectionField`'s field type's native Python type. """ if value is not None: values = [] for item in value: values.append(self.field.flatten(item)) return values
[docs] def jsonify(self, value): """ Convert all values of an iterable to the :class:`CollectionField`'s field type's JSON type. """ if value is not None: values = [] for item in value: values.append(self.field.jsonify(item)) return values #: Map basic types to fields
_type_map = { datetime: DateTimeField(), int: IntField(), basestring: StringField(), unicode: StringField(), str: StringField(), float: FloatField(), bool: BooleanField(), } def get_field_for_type(obj): t = type(obj) if t in _type_map: return _type_map[t] for typ, field in _type_map.items(): if isinstance(obj, typ): return field return None