Source code for tg_option_container.types

import datetime
import inspect

from gettext import gettext as _

import dateutil.parser


class InvalidOption(AttributeError):
    """Special exception used when option validation fails
    """

    def __init__(self, message, **kwargs):
        self.message = message
        self.format_params = kwargs

        super(InvalidOption, self).__init__(message)

    def add_params(self, **kwargs):
        self.format_params.update(kwargs)

    def __str__(self):
        msg = self.message

        if self.format_params:
            msg = msg.format(**self.format_params)

        return msg


class MinValueValidator(object):
    """Validate the value is greater or equal to I{min_value}
    """

    def __init__(self, min_value):
        self.min_value = min_value

    def __str__(self):
        return '<MinValueValidator min_value={0}>'.format(self.min_value)

    def __call__(self, value):
        if value < self.min_value:
            raise InvalidOption(_('Ensure value for option `{key}` is greater than or equal to {min_value}'), min_value=self.min_value)

        return True


class MaxValueValidator(object):
    """Validate the value is less or equal to I{max_value}
    """

    def __init__(self, max_value):
        self.max_value = max_value

    def __str__(self):
        return '<MaxValueValidator max_value={0}>'.format(self.max_value)

    def __call__(self, value):
        if value > self.max_value:
            raise InvalidOption(_('Ensure value for option `{key}` is less than or equal to {max_value}'), max_value=self.max_value)

        return True


class ChoicesValidator(object):
    """Validate the value is in I{choices}
    """

    def __init__(self, choices):
        if isinstance(choices, list):
            choices = tuple(choices)

        try:
            from model_utils import Choices

        except ImportError:
            pass

        else:
            if isinstance(choices, Choices):
                choices = tuple([db_value for db_value, code_identifier in choices])

        assert isinstance(choices, tuple)

        self.choices = choices

    def __str__(self):
        return '<ChoicesValidator choices={0}>'.format(self.choices)

    def __call__(self, value):
        if value not in self.choices:
            raise InvalidOption(_('Invalid choice {value} for option `{key}`, choices are {choices}.'), value=value, choices=self.choices)

        return True


class TypeValidator(object):
    """Validate the value is an instance of I{expected_type}
    """

    def __init__(self, expected_type, prepend='', append=''):
        self.append = append
        self.prepend = prepend
        self.expected_type = expected_type

        # If this is a callable, Option class will append it to cleaners automatically
        self.clean = None

    def __str__(self):
        return '<TypeValidator expected_type={0}{1}{2}>'.format(
            self.expected_type,
            ' prepend={0}'.format(self.prepend) if self.prepend else '',
            ' append={0}'.format(self.append) if self.append else '',
        )

    def __call__(self, value):
        if not isinstance(value, self.expected_type):
            raise InvalidOption(_('{prepend}Expected type {expected_type} for option `{key}`, provided type is {value_type}.{append}'),
                                value_type=type(value),
                                expected_type=self.expected_type,
                                prepend='{0} '.format(self.prepend) if self.prepend else '',
                                append=' {0}'.format(self.append) if self.append else '')

        return True


class ListValidator(TypeValidator):
    """Validate the value is an instance of list and all items of it are I{expected_type}
    """

    def __init__(self, expected_type, allow_empty=True):
        self.allow_empty = allow_empty

        super(ListValidator, self).__init__(expected_type=expected_type)

        self.clean = self._clean

    def __str__(self):
        return '<ListValidator expected_type={0} allow_empty={1}>'.format(
            self.expected_type,
            self.allow_empty,
        )

    def _clean(self, value):
        from .container import OptionContainer

        # This is just a sanity check
        if not isinstance(value, list):
            raise InvalidOption(_('Expected type {expected_type} for option `{key}`, provided type is {value_type}.'),
                                value_type=type(value),
                                expected_type=list)

        if self.expected_type is not None:
            if inspect.isclass(self.expected_type) and issubclass(self.expected_type, OptionContainer):
                # Expected type is an OptionContainer, lets try to construct it
                value = [self.expected_type(**params) if not isinstance(params, self.expected_type) else params for params in value]

            elif isinstance(self.expected_type, Option):
                # Expected type is an Option, lets use it to validate our value
                value = [self.expected_type.validate(inner) for inner in value]

        return value

    def __call__(self, value):
        if not isinstance(value, list):
            raise InvalidOption(_('Expected type {expected_type} for option `{key}`, provided type is {value_type}.'),
                                value_type=type(value),
                                expected_type=list)

        if self.expected_type is not None:
            if isinstance(self.expected_type, Option):
                if not all([self.expected_type.is_valid(x) for x in value]):
                    raise InvalidOption(_('Expected all items in list to be OPTION: {expected_type} for option `{key}`.'),
                                        expected_type=self.expected_type)

            else:
                if not all([isinstance(x, self.expected_type) for x in value]):
                    raise InvalidOption(_('Expected all items in list to be {expected_type} for option `{key}`.'),
                                        expected_type=self.expected_type)

        return True


def clean_datetime(value):
    if value is not None:
        # Also support some more human readable variants of iso8601
        if isinstance(value, str):
            value = value.replace(' +', '+')
            value = value.replace(' Z', 'Z')

            value = dateutil.parser.parse(value)

    return value


def clean_option_container(container_cls):
    from .container import OptionContainer

    def _clean_option_container(value):
        try:
            if isinstance(value, dict):
                return container_cls(**value)

            elif isinstance(value, OptionContainer):
                if not isinstance(value, container_cls):
                    raise InvalidOption(_('Provided OptionContainer instance {value} is not a subclass {container_cls}'),
                                        value=value,
                                        container_cls=container_cls)

                return value

            else:
                return container_cls()

        except InvalidOption as e:
            raise InvalidOption('{key}:{inner}', inner=str(e))

    setattr(_clean_option_container, 'container_cls', container_cls)

    return _clean_option_container


class Undefined(object):  # pragma: no cover
    """
    This class is used to represent no data being provided for a given option value.

    It is required because `None` may be a valid value in some cases.
    """
    def __str__(self):
        return 'undefined'

    def __repr__(self):
        return self.__str__()


[docs]class Option(object): """Single option definition for option containers Attributes: name (str): The option name default (any): The default value validators (callable): Callable with signature `fn(value) -> bool` used to validate the input value (can also raise InvalidOption) clean (callable): Callable with signature `fn(value) -> any` used to clean the value before running I{validators} (can also be a list of callables). choices: If provided adds ChoicesValidator to I{validators} expected_type: If provided adds TypeValidator to I{validators}. This can also be an instance of TypeValidator (or a subclass instance). min_value: If provided adds MinValueValidator to I{validators} max_value: If provided adds MaxValueValidator to I{validators} none_to_default: If provided `None` will be treated as `Undefined` (cleaned to default) resolve_default: If provided default will be treated as a callable """ def __init__(self, name, default, validators=None, clean=None, **kwargs): assert isinstance(name, str), \ 'Name should be a string' self.name = name self.default = default self.validators = [] self.clean = [] if clean is not None: if not isinstance(clean, list): clean = [clean, ] assert all([callable(x) for x in clean]) self.clean = clean if validators is not None: if not isinstance(validators, list): validators = [validators, ] assert all([callable(x) for x in validators]) self.validators = validators # Handle expected_type kwarg expected_type = kwargs.get('expected_type', None) expected_type__prepend = kwargs.get('expected_type__prepend', '') expected_type__append = kwargs.get('expected_type__append', '') if expected_type is not None: if not isinstance(expected_type, TypeValidator): expected_type = TypeValidator( expected_type=expected_type, prepend=expected_type__prepend, append=expected_type__append ) self.validators.insert(0, expected_type) validator_clean = getattr(expected_type, 'clean', None) if validator_clean is not None: self.clean.insert(0, validator_clean) # Handle choices kwarg choices = kwargs.get('choices', None) if choices is not None: self.validators.insert(0, ChoicesValidator(choices=choices)) # Handle min_value kwarg min_value = kwargs.get('min_value', None) if min_value is not None: self.validators.append( MinValueValidator(min_value=min_value) ) # Handle max_value kwarg max_value = kwargs.get('max_value', None) if max_value is not None: self.validators.append( MaxValueValidator(max_value=max_value) ) # Handle none_to_default kwarg self.none_to_default = kwargs.get('none_to_default', False) def __str__(self): return "<{cls} {name}: default={default}, {typedef}>".format( cls=self.__class__.__name__, name=self.name, default=self.default, typedef=self.typedef, ) def __repr__(self): # pragma: no cover return self.__str__() @property def typedef(self): return "validators={validators} clean={clean}".format( clean=self.clean, validators=[str(x) for x in self.validators], ) def _nvl(self, value=Undefined()): """Convert values to I{default} when value is Undefined""" # If self.none_to_default is True, we convert None to Undefined() if self.none_to_default and value is None: value = Undefined() # If value is not defined, return the default, else the value if isinstance(value, Undefined): return self.default else: return value def _run_clean(self, value): """Run all I{clean} on the value""" for clean in self.clean: value = clean(value) return value def is_valid(self, value): try: self._run_validators(value) except InvalidOption: return False else: return True def _run_validators(self, value): for validator in self.validators: if not validator(value): raise InvalidOption('Invalid value `{value}` for option `{key}`', value=value)
[docs] def validate(self, value): """Clean and validate the provided value against I{validators} Args: value: Value to validate """ value = self._nvl(value) value = self._run_clean(value) # Run validators on the cleaned value self._run_validators(value) # Return the cleaned value return value
@classmethod
[docs] def integer(cls, name, default, validators=None, clean=None, **kwargs): """Option of integer type Note: This is a shorthand for: Option(..., expected_type=int) Args: name: see Option.__init__ default: see Option.__init__ validators: see Option.__init__ clean: see Option.__init__ **kwargs: see Option.__init__ """ kwargs.setdefault('expected_type', int) return Option(name, default, validators=validators, clean=clean, **kwargs)
@classmethod
[docs] def boolean(cls, name, default, validators=None, clean=None, **kwargs): """Option of boolean type Note: This is a shorthand for: Option(..., expected_type=bool) Args: name: see Option.__init__ default: see Option.__init__ validators: see Option.__init__ clean: see Option.__init__ **kwargs: see Option.__init__ """ kwargs.setdefault('expected_type', bool) return Option(name, default, validators=validators, clean=clean, **kwargs)
@classmethod
[docs] def string(cls, name, default, validators=None, clean=None, **kwargs): """Option of string type Note: This is a shorthand for: Option(..., expected_type=str) Args: name: see Option.__init__ default: see Option.__init__ validators: see Option.__init__ clean: see Option.__init__ **kwargs: see Option.__init__ """ kwargs.setdefault('expected_type', str) return Option(name, default, validators=validators, clean=clean, **kwargs)
@classmethod
[docs] def iso8601(cls, name, default, validators=None, clean=None, **kwargs): """Option of iso8601 type Note: This is a shorthand for: Option(..., expected_type=datetime.datetime, expected_type__append=_('Please use ISO_8601.'), clean=clean_datetime) Accepts the following formats: - ISO_8601 - ISO_8601 with spaces: 2016-05-09 16:00:00 +02:00 Note: For both variants the timezone part is optional and defaults to UTC Args: name: see Option.__init__ default: see Option.__init__ validators: see Option.__init__ clean: see Option.__init__ **kwargs: see Option.__init__ """ kwargs.setdefault('expected_type', datetime.datetime) kwargs.setdefault('expected_type__append', _('Please use ISO_8601.')) if not clean: clean = [] if not isinstance(clean, list): clean = [clean, ] clean.append(clean_datetime) return Option(name, default, validators=validators, clean=clean, **kwargs)
@classmethod
[docs] def list(cls, name, default, validators=None, clean=None, inner_type=None, allow_empty=True, **kwargs): """Option of list type Note: This is a shorthand for: Option(..., expected_type=ListValidator(inner_type, autoclean, allow_empty)) Args: inner_type (any): Can be used to construct a typed list allow_empty (Optional[bool]): If False ListValidator will also check that the list is not empty. Defaults to **True** name: see Option.__init__ default: see Option.__init__ validators: see Option.__init__ clean: see Option.__init__ **kwargs: see Option.__init__ """ from .container import OptionContainer # Construct a list if list is provided if callable(default): kwargs.setdefault('resolve_default', True) kwargs.setdefault('expected_type', ListValidator(inner_type, allow_empty)) res = Option(name, default, validators=validators, clean=clean, **kwargs) if inspect.isclass(inner_type) and issubclass(inner_type, OptionContainer): # This is for pretty printing and as_dict setattr(res, '_list_of_containers', True) return res
@classmethod
[docs] def nested(cls, name, container_cls, validators=None, clean=None, **kwargs): """Option of string type Note: This is a shorthand for: Option(..., expected_type=container_cls, clean=clean_option_container(container_cls)) Args: container_cls: The option container to nest name: see Option.__init__ validators: see Option.__init__ clean: see Option.__init__ **kwargs: see Option.__init__ """ kwargs['expected_type'] = container_cls if not clean: clean = [] if not isinstance(clean, list): clean = [clean, ] clean.append(clean_option_container(container_cls)) opt = Option(name, {}, validators=validators, clean=clean, **kwargs) setattr(opt, '_is_nested', True) return opt