"""General utility functions."""
import re
from functools import update_wrapper
from typing import Any, Callable, Dict, List, Optional
import numpy as np
import pandas as pd
from multimethod import multimethod
[docs]
def camel_to_snake_case(value: str) -> str:
    """Transform the camel case string into a snake one.
    Parameters
    ----------
    value
        The string to process
    Returns
    -------
    str
        The passed string in lower case camel format.
    Raises
    ------
    TypeError
        When the given value is not a string.
    """
    if not isinstance(value, str):
        raise TypeError("value must be a string")
    return re.sub("([a-z])([A-Z])", r"\1_\2", value).lower() 
[docs]
def to_camel_case(value: str) -> str:
    """
    Transform a string into a camel case string.
    Parameters
    ----------
    value
        The string to process.
    Returns
    -------
    str
        The passed string in camel case format.
    """
    name = re.split("([^a-zA-Z0-9])", value)
    return name[0].lower() + "".join(a.capitalize() for a in name[1:] if a.isalnum()) 
[docs]
def drop_none(data: List[Any]) -> List[Any]:
    """Drop ``None`` elements from a list."""
    return list(filter(lambda x: x is not None, data)) 
[docs]
def convert_column_types(
    data: pd.DataFrame, column_type_fetcher: Callable[[Any], str]
) -> pd.DataFrame:
    """Convert columns of a pandas data frame to their indicated types.
    Parameters
    ----------
    data
        The data frame to be converted
    column_type_fetcher
        A function that retrieves the column type given its name.
    Returns
    -------
    pandas.DataFrame
        A pandas data frame containing the converted columns.
    """
    # change data types if needed
    for column, type_ in data.dtypes.iteritems():
        try:
            expected_type = np.dtype(column_type_fetcher(column))
        except Exception:  # pylint: disable=broad-except
            expected_type = np.dtype("U")
        if expected_type != type_:
            if np.issubdtype(expected_type, np.datetime64):
                try:
                    data[column] = pd.to_datetime(data[column], unit="ms")
                except ValueError:
                    data[column] = data[column].astype(expected_type)
            elif np.issubsctype(expected_type, bool):
                data[column] = data[column].replace(
                    {"true": True, "false": False, "True": True, "False": False}
                )
            elif np.issubsctype(expected_type, np.float32):
                data[column] = (
                    data[column]
                    .apply(lambda x: None if x == "null" else x)
                    .astype(expected_type)
                )
            else:
                data[column] = data[column].astype(expected_type)
    return data 
[docs]
def raise_multiple_errors(errors: List[Exception]):
    """Re-raise multiple exceptions one after the other.
    Parameters
    ----------
    errors
        List of exceptions to raise
    Raises
    ------
    Exception
        The exceptions given as input
    """
    if len(errors) == 0:
        return
    try:
        raise errors.pop(0)
    finally:
        raise_multiple_errors(errors) 
[docs]
def plural(single: str, count: int, multiple: Optional[str] = None) -> str:
    """Associate the word with its count.
    Parameters
    ----------
    single
        The single word.
    count
        The count of the given count.
    multiple
        The plural word of single if it shouldn't just end with an `s`.
    Returns
    -------
    str
        Its associated plural.
    """
    if multiple is None:
        multiple = single + ("s" if count != 1 else "")
    return f"{count} {single if count == 1 else multiple}" 
[docs]
class multidispatchmethod:  # pylint: disable=invalid-name
    """Multi-dispatch generic method descriptor."""
[docs]
    def __init__(self, func: Callable[..., Any]):
        if not callable(func) and not hasattr(func, "__get__"):
            raise TypeError(f"{func} is not callable or a descriptor")
        self.dispatcher = multimethod(func)
        self.func = func 
[docs]
    def register(self, cls, method=None):
        """Register the method."""
        return self.dispatcher.register(cls, method) 
    def __get__(self, obj, cls):
        def _method(*args, **kwargs):
            method = self.dispatcher[tuple(map(self.dispatcher.get_type, args))]
            return method(obj, *args, **kwargs)
        _method.__isabstractmethod__ = self.__isabstractmethod__
        _method.register = self.register
        update_wrapper(_method, self.func)
        return _method
    @property
    def __isabstractmethod__(self) -> bool:
        return getattr(self.func, "__isabstractmethod__", False) 
[docs]
def set_attributes_from_kwargs(
    obj: object, *attrs: str, pop: bool = True, **kwargs
) -> Dict[str, Any]:
    """Set attributes in object from kwargs.
    Parameters
    ----------
    obj
        The class object where the attributes are to be set.
    attrs
        The names of teh attributes that are to be set in the object.
    pop
        ``True`` if the attributes are to popped from the provided keyword arguments.
        ``False`` otherwise.
    kwargs
        The keyword argument from which the attributes are to be retrieved as well. If
        no values corresponding to the provided attributes are found a ``None`` value is
        set instead.
    Returns
    -------
    Dict[str, Any]
        A dictionary of the remaining key word arguments.
    """
    func = getattr(kwargs, "pop" if pop else "get")
    for attribute in attrs:
        kwargs_attribute = func(attribute, None)
        new_attribute = kwargs_attribute or getattr(obj, attribute, None)
        setattr(obj, attribute, new_attribute)
    return kwargs