"""Implements decorators used throughout the library."""
import json
from functools import wraps
from validator_collection import checkers
from highcharts_core import errors, constants
[docs]def validate_types(value,
                   types = None,
                   allow_dict = True,
                   allow_json = True,
                   allow_none = True,
                   allow_js_literal = True,
                   force_iterable = False,
                   function_name = None):
    """Validates that ``value`` is one or more of the allowed types, where the first
    type passed in ``types`` is the primary type that it will be returned as.
    :param value: The value to be validated.
    :type value: Any
    :param types: :class:`type <python:type>` object or iterable of
      :class:`type <python:type>` objects used to indicate which types are allowed. The
      first (or only) item in ``types`` will indicate the primary type that ``value`` will
      be returned as.
    :types: :class:`type <python:type>` or iterable of :class:`type <python:type>`
    :param allow_dict: If ``True``, will accept a :class:`dict <python:dict>` object as
      ``value``. Defaults to ``True``.
    :type allow_dict: :class:`bool <python:bool>`
    :param allow_json: If ``True``, will accept a :class:`str <python:str>` object as
      ``value``, under the assumption that is is deserializable as JSON. Defaults to
      ``True``.
    :type allow_json: :class:`bool <python:bool>`
    :param allow_none: If ``True``, will accept an empty or :obj:`None <python:None>`
      object as ``value``. Defaults to ``True``.
    :type allow_none: :class:`bool <python:bool>`
    :param allow_js_literal: If ``True``, will accept a :class:`str <python:str>` object
      as ``value``, under the assumption that it is deserializable as a JavaScript object
      literal. Defaults to ``True``.
    :type allow_js_literal: :class:`bool <python:bool>`
    :param force_iterable: If ``True``, will accept an iterable object as ``value``.
      Defaults to ``False`` (because most attributes are just singletons).
    :type force_iterable: :class:`bool <python:bool>`
    :param function_name: The optional name of the function that was originally called.
    :type function_name: :class:`str <python:str>`
    :returns: ``value`` de-serialized to the primary type (first
      :class:`type <python:type>` in ``types``)
    :rtype: first :class:`type <python:type>` in ``types``
    :raises HighchartsImplementationError: if ``types`` is empty
    :raises HighchartsValueError: if ``types`` does not contain a
      :class:`type <python:type>` or iterable of :class:`type <python:type>` objects
    :raises HighchartsValueError: if the primary type does not conform to the
      :class:`HighchartsMeta` interface definition
    """
    if not types:
        raise errors.HighchartsImplementationError('types cannot be empty - must be a type or '
                                         'iterable of types')
    if not types:
        raise errors.HighchartsImplementationError('types cannot be empty - must be a type or '
                                         'iterable of types')
    try:
        types_list = [x for x in types]
    except TypeError:
        types_list = [types]
    for item in types_list:
        if not isinstance(item, type):
            raise errors.HighchartsValueError(f'types must contain one or more type '
                                              f'objects. Received a {type(item)}.')
    primary_type = types_list[0]
    if not hasattr(primary_type, 'from_js_literal'):
        allow_js_literal = False
    if allow_none and force_iterable and checkers.is_iterable(value) and not value:
        value = []
    elif allow_none and isinstance(value, constants.EnforcedNullType):
        pass
    elif allow_none and not value:
        value = None
    elif not allow_none and not value:
        raise errors.HighchartsValueError('value is not expected to be empty, but was '
                                          'empty')
    elif allow_dict and isinstance(value, dict):
        try:
            value = primary_type.from_dict(value)
        except AttributeError as error:
            if not hasattr(primary_type, 'from_dict'):
                raise errors.HighchartsValueError(f'supplied type '
                                                  f'({primary_type.__name__}) '
                                                  f'does not conform to the '
                                                  f'HighchartsMeta interface')
            else:
                raise error
    elif allow_js_literal and isinstance(value, str):
        try:
            value = primary_type.from_js_literal(value)
        except ValueError:
            pass
    if allow_json and isinstance(value, (str, bytes)):
        try:
            value = primary_type.from_json(value)
        except AttributeError:
            raise errors.HighchartsValueError(f'supplied type '
                                              f'({primary_type.__class__.__name__}) '
                                              f'does not conform to the '
                                              f'HighchartsMeta interface')
        except TypeError:
            value = json.loads(value)
    if force_iterable and checkers.is_iterable(value):
        value = [validate_types(x,
                                types = types,
                                allow_dict = allow_dict,
                                allow_json = allow_json,
                                allow_js_literal = allow_js_literal,
                                force_iterable = force_iterable)
                 for x in value]
    elif allow_none and isinstance(value, constants.EnforcedNullType):
        pass
    elif allow_none and not value:
        pass
    elif not isinstance(value, primary_type):
        error_string = f'expects a {primary_type.__name__}'
        if function_name:
            error_string = f'{function_name} ' + error_string
        allow_js = allow_json or allow_js_literal
        if allow_dict and allow_js and force_iterable and allow_none:
            error_string += ', dict, str, iterable, or empty object.'
        elif allow_dict and allow_js and force_iterable:
            error_string += ', dict, str, or iterable object.'
        elif allow_dict and allow_js and allow_none:
            error_string += ', dict, str, or empty object.'
        elif allow_dict and force_iterable and allow_none:
            error_string += ', dict, iterable, or empty object.'
        elif allow_dict and allow_js:
            error_string += ', dict, or str object.'
        elif allow_dict and force_iterable:
            error_string += ', dict, or iterable object.'
        elif allow_dict and allow_none:
            error_string += ', dict, or empty object.'
        elif allow_js and force_iterable:
            error_string += ', str, or iterable object.'
        elif allow_js and allow_none:
            error_string += ', str, or empty object.'
        elif force_iterable and allow_none:
            error_string += ', iterable, or empty object.'
        elif allow_js:
            error_string += ' or str object.'
        elif force_iterable:
            error_string += ' or iterable object.'
        elif allow_none:
            error_string += ' or empty object.'
        error_string += f' Received {value.__class__.__name__}.'
        raise errors.HighchartsValueError(error_string)
    return value 
[docs]def class_sensitive(types = None,
                    allow_dict = True,
                    allow_json = True,
                    allow_none = True,
                    allow_js_literal = True,
                    force_iterable = False):
    """Validates that the values passed to a decorated function or method are
    de-serialized to the appropriate type.
    :param types: :class:`type <python:type>` object or iterable of
      :class:`type <python:type>` objects used to indicate which types are allowed. The
      first (or only) item in ``types`` will indicate the primary type that ``value`` will
      be returned as.
    :types: :class:`type <python:type>` or iterable of :class:`type <python:type>`
    :param allow_dict: If ``True``, will accept a :class:`dict <python:dict>` object as
      ``value``. Defaults to ``True``.
    :type allow_dict: :class:`bool <python:bool>`
    :param allow_json: If ``True``, will accept a :class:`str <python:str>` object as
      ``value``, under the assumption that is is deserializable as JSON. Defaults to
      ``True``.
    :type allow_json: :class:`bool <python:bool>`
    :param allow_none: If ``True``, will accept an empty or :obj:`None <python:None>`
      object as ``value``. Defaults to ``True``.
    :type allow_none: :class:`bool <python:bool>`
    :param allow_js_literal: If ``True``, will accept a :class:`str <python:str>` object
      as ``value``, under the assumption that it is deserializable as a JavaScript object
      literal. Defaults to ``True``.
    :type allow_js_literal: :class:`bool <python:bool>`
    :param force_iterable: If ``True``, will accept an iterable object as ``value``.
      Defaults to ``False`` (because most attributes are just singletons).
    :type force_iterable: :class:`bool <python:bool>`
    .. note::
      To apply the decorator to a property setter method (the most-common use case), place
      it *after* the ``@<property name>.setter`` decorator and directly above the function
      name like so:
      .. code-block:: python
        @some_property.setter
        @class_sensitive(...)
        def some_property(self, value):
            ...
    :returns: The result of the decorated function or method having validated the class
      typing.
    :raises HighchartsImplementationError: if ``types`` is empty
    :raises HighchartsValueError: if ``types`` does not contain a
      :class:`type <python:type>` or iterable of :class:`type <python:type>` objects
    :raises HighchartsValueError: if the primary type does not conform to the
      :class:`HighchartsMeta` interface definition
    """
    def decorator(func):
        @wraps(func)
        def func_wrapper(*args,
                         **kwargs):
            function_name = func.__name__
            try:
                value = args[1]
            except IndexError:
                raise errors.HighchartsError('Something went wrong. Unsure how this '
                                             'might happen.')
            value = validate_types(value,
                                   types = types,
                                   allow_dict = allow_dict,
                                   allow_json = allow_json,
                                   allow_none = allow_none,
                                   allow_js_literal = allow_js_literal,
                                   force_iterable = force_iterable,
                                   function_name = function_name)
            result = func(args[0], value)
            return result
        return func_wrapper
    return decorator