Source code for campos.sources

import abc
import re
import inspect
import datetime as dt
from collections import OrderedDict

from .. import fields
from ..utils import callable
from ..validators import (NumberRange, StringLength, DateRange, TimeRange,
                          DatetimeRange)

__author__ = 'Juan Manuel Bermúdez Cabrera'

OLDEST_PERSON = 122  # years
TALLEST_PERSON = 2.72  # m
HEAVIEST_PERSON = 635  # kg


[docs]class FieldSource(metaclass=abc.ABCMeta): """Base class for field extractors. Inspects an object looking for valid attributes to create fields, callables are always ignored and extra filters can be provided using keyword arguments(``exclude``, ``under``, ``dunder``). Field's text can be modified by setting ``prettify=True`` (default) or providing a custom transformation function using ``apply`` keyword. The following steps are performed when extracting valid object attributes to create fields: * Non-callable attributes and their values are extracted from the provided object. * Attribute names are filtered using expressions in ``exclude`` and values of ``under`` (beginning with exactly one _) and ``dunder`` (beginning with two or more _). * A transformation function(if provided through ``apply`` keyword) is used to obtain a nice text for the future field using attribute's name. * Some simple and common transformations such as _ removal and text capitalization are done upon attributes names in order to obtain a nice text for the future field. Note this step is performed even if a transformation function is provided, to disable this behavior set ``pretiffy=False``. * Finally only attributes with supported python types can pass, see :attr:`SUPPORTED_TYPES`. Subclasses must implement this check accordingly since some objects may contain wrapped python types. Fields are created after this filtering process, this is done by calling an utility function designed for each supported python type. See :func:`from_bool`, :func:`from_int`, :func:`from_str`, etc. Subclasses must implement :func:`create_fields` for this. :param obj: object to extract fields from :type obj: any :param exclude: regular expressions to exclude, attribute names matching any of these will be ignored. :type exclude: iterable of :class:`str` or compiled :mod:`re` :param under: whether to allow or not attribute names beginning with exactly one _. Defaults to False. :param dunder: whether to allow or not attribute names beginning with two or more _. Defaults to False. :param prettify: perform some simple and common transformations such as _ removal and text capitalization upon attributes names in order to obtain a nice text for the future field. Defaults to True. :param apply: transformation function to apply to each attribute name to obtain a nice text for the future field. Note that if ``prettify=True`` this is done after that step. :type apply: callable """ #: Supported python types. SUPPORTED_TYPES = (int, float, str, bool, dt.date, dt.time, dt.datetime) def __init__(self, obj, exclude=(), under=False, dunder=False, prettify=True, apply=None): self.object = obj attributes = OrderedDict() # attr_name --> (text, value) # 1: exclude functions for attr, value in self.get_members(): if not callable(value): attributes[attr] = attr, value # 2: exclude attributes with unwanted names excluded_patterns = self._excluded_patterns(exclude, under, dunder) for attr in tuple(attributes.keys()): for pattern in excluded_patterns: if pattern.fullmatch(attr): attributes.pop(attr) break # 3: apply given transformation function(if there is one) if callable(apply): for attr, text_value in attributes.items(): text = text_value[0] value = text_value[1] attributes[attr] = apply(text), value # 4: prettify attribute's text if prettify: for attr, text_value in attributes.items(): text = text_value[0] value = text_value[1] attributes[attr] = self._prettify(text), value self.fields = self.create_fields(attributes)
[docs] def get_members(self): """Returns all members of the source object, along with their value. :return: a list of tuples (member_name, member_value) :rtype: :class:`list` """ return inspect.getmembers(self.object)
@abc.abstractmethod
[docs] def create_fields(self, attributes): """Creates new fields from ``attributes``. Subclasses must implement this and check if type of each attribute is a supported python type, see :attr:`SUPPORTED_TYPES`. In order to create new fields the from_* methods in this module can can be useful. :param attributes: a dict like d[attr_name] = (attr_text, attr_value) where attr_name will be the new field's name and attr_text its nice text. :type attributes: :class:`dict` :return: a dict like d[field_name] = field :rtype: :class:`dict` """ pass
@staticmethod def _excluded_patterns(exclude, under, dunder): patterns = [] if not under: patterns.append(re.compile(r'_')) patterns.append(re.compile(r'_[^_]+.*')) if not dunder: patterns.append(re.compile(r'__')) patterns.append(re.compile(r'_{2,}[^_]+.*')) for exp in exclude: pattern = exp if isinstance(exp, str): pattern = re.compile(exp) patterns.append(pattern) return patterns @staticmethod def _prettify(text): text = text.lstrip('_') # remove leading _ text = text.rstrip('_') # remove trailing _ text = text.replace('_', ' ') # replace internal _ with space text = text.capitalize() return text
[docs]def get_fields_source(arg, **source_kw): """Tries to find the best :class:`FieldSource` for the given argument. An :class:`~object.ObjectSource` is always returned when a more adequate field source isn't found. :param arg: object to find the right field source for. :param arg: any :param source_kw: keyword arguments to pass to the ``FieldSource`` constructor. :return: a new field source object :rtype: :class:`FieldSource` :raises ValueError: if argument is ``None`` """ msg = "Can't obtain a field source from {}".format(arg) if arg is None: raise ValueError(msg) source = None try: # if SQLAlchemy can't be imported it's pretty sure arg is not a # SQLAlchemy object from sqlalchemy import Table except ImportError: pass else: # check if arg is a SQLAlchemy object if isinstance(arg, Table) or hasattr(arg, '__table__'): from .sqlalchemy import SQLAlchemySource source = SQLAlchemySource(arg, **source_kw) if source is None: from .object import ObjectSource source = ObjectSource(arg, **source_kw) return source
[docs]def from_bool(name, text, value, **kwargs): """Creates a ``BoolField`` from a boolean value. :param name: name for the field :type name: :class:`str` :param text: text for the field :type text: :class:`str` :param value: a boolean value :type value: :class:`bool` :param kwargs: keyword arguments to pass to field constructor :return: a new ``BoolField`` with the given name and text :rtype: :class:`~campos.fields.BoolField` """ fkwargs = kwargs.copy() fkwargs['name'] = name fkwargs['text'] = text return fields.BoolField(**fkwargs)
[docs]def from_int(name, text, value, **kwargs): """Creates a ``IntField`` from an integer value. A bit of logic is applied using provided arguments to determine some field's settings. :param name: name for the field :type name: :class:`str` :param text: text for the field :type text: :class:`str` :param value: integer value to help adjust field's settings :type value: :class:`int` :param kwargs: keyword arguments to pass to field constructor :return: a new ``IntField`` with the given name and text :rtype: :class:`~campos.fields.IntField` """ fkwargs = kwargs.copy() fkwargs['name'] = name fkwargs['text'] = text if text.lower() == 'age' and 0 <= value <= OLDEST_PERSON: fkwargs['min'] = kwargs.get('min', 0) fkwargs['max'] = kwargs.get('max', OLDEST_PERSON) fkwargs['step'] = kwargs.get('step', 1) else: val = NumberRange() # if default range isn't enough use given value with a span of 100 units if val.min is None or value <= val.min: fkwargs['min'] = kwargs.get('min', value - 100) if val.max is None or value >= val.max: fkwargs['max'] = kwargs.get('max', value + 100) return fields.IntField(**fkwargs)
[docs]def from_float(name, text, value, **kwargs): """Creates a ``FloatField`` from a float value. A bit of logic is applied using provided arguments to determine some field's settings. :param name: name for the field :type name: :class:`str` :param text: text for the field :type text: :class:`str` :param value: float value to help adjust field's settings :type value: :class:`float` :param kwargs: keyword arguments to pass to field constructor :return: a new ``FloatField`` with the given name and text :rtype: :class:`~campos.fields.FloatField` """ fkwargs = kwargs.copy() fkwargs['name'] = name fkwargs['text'] = text if text.lower() == 'height' and 0 <= value <= TALLEST_PERSON: fkwargs['min'] = kwargs.get('min', 0) fkwargs['max'] = kwargs.get('max', TALLEST_PERSON) fkwargs['step'] = kwargs.get('step', 0.5) elif text.lower() == 'weight' and 0 <= value <= HEAVIEST_PERSON: fkwargs['min'] = kwargs.get('min', 0) fkwargs['max'] = kwargs.get('max', HEAVIEST_PERSON) fkwargs['step'] = kwargs.get('step', 1) else: val = NumberRange() # if default range isn't enough use given value with a span of 100 units if val.min is None or value <= val.min: fkwargs['min'] = kwargs.get('min', value - 100) if val.max is None or value >= val.max: fkwargs['max'] = kwargs.get('max', value + 100) return fields.FloatField(**fkwargs)
[docs]def from_str(name, text, value, istext=False, **kwargs): """Creates a ``StringField`` or a ``TextField`` from a string value. A bit of logic is applied using provided arguments to determine some field's settings and if output will be a string field or a text field. If you want the output to be a ``TextField`` set ``istext=True``. :param name: name for the field :type name: :class:`str` :param text: text for the field :type text: :class:`str` :param value: string value to help adjust field's settings :type value: :class:`str` :param istext: if True forces output to be a ``TextField`` :type istext: :class:`bool` :param kwargs: keyword arguments to pass to field constructor :return: a new ``StringField`` or ``TextField`` with the given name and text :rtype: :class:`~campos.fields.StringField` or :class:`~campos.fields.TextField` """ fkwargs = kwargs.copy() fkwargs['name'] = name fkwargs['text'] = text fkwargs['min_length'] = kwargs.get('min_length', 0) val = StringLength() if istext or len(value) > val.max or '\n' in value: return fields.TextField(**fkwargs) return fields.StringField(**fkwargs)
[docs]def from_date(name, text, value, **kwargs): """Creates a ``DateField`` from a date object. A bit of logic is applied using provided arguments to determine some field's settings. :param name: name for the field :type name: :class:`str` :param text: text for the field :type text: :class:`str` :param value: date object to help adjust field's settings :type value: :class:`datetime.date` :param kwargs: keyword arguments to pass to field constructor :return: a new ``DateField`` with the given name and text :rtype: :class:`~campos.fields.DateField` .. seealso:: :func:`from_time` and :func:`from_datetime` """ fkwargs = kwargs.copy() fkwargs['name'] = name fkwargs['text'] = text val = DateRange() if value < val.min and 'min' not in fkwargs: fkwargs['min'] = value return fields.DateField(**fkwargs)
[docs]def from_time(name, text, value, **kwargs): """Creates a ``TimeField`` from a time object. A bit of logic is applied using provided arguments to determine some field's settings. :param name: name for the field :type name: :class:`str` :param text: text for the field :type text: :class:`str` :param value: time object to help adjust field's settings :type value: :class:`datetime.time` :param kwargs: keyword arguments to pass to field constructor :return: a new ``TimeField`` with the given name and text :rtype: :class:`~campos.fields.TimeField` .. seealso:: :func:`from_date` and :func:`from_datetime` """ fkwargs = kwargs.copy() fkwargs['name'] = name fkwargs['text'] = text val = TimeRange() if value < val.min and 'min' not in fkwargs: fkwargs['min'] = value return fields.TimeField(**fkwargs)
[docs]def from_datetime(name, text, value, **kwargs): """Creates a ``DatetimeField`` from a datetime object. A bit of logic is applied using provided arguments to determine some field's settings. :param name: name for the field :type name: :class:`str` :param text: text for the field :type text: :class:`str` :param value: datetime object to help adjust field's settings :type value: :class:`datetime.datetime` :param kwargs: keyword arguments to pass to field constructor :return: a new ``DatetimeField`` with the given name and text :rtype: :class:`~campos.fields.DatetimeField` .. seealso:: :func:`from_date` and :func:`from_time` """ fkwargs = kwargs.copy() fkwargs['name'] = name fkwargs['text'] = text val = DatetimeRange() if value < val.min and 'min' not in fkwargs: fkwargs['min'] = value return fields.DatetimeField(**fkwargs)