Decorates a function and handles any exceptions that may rise.

    Args:
        actions: A dictionary ``<exception type>: <action>``. Available actions\
            are :class:`raises` and :class:`returns`.

    Returns:
        Any value declared using a :class:`returns` action.

    Raises:
        AnyException: if AnyException is declared together with a
            :class:`raises` action.
        InterceptorError: if the decorator is called with something different
            from a :class:`returns` or :class:`raises` action.

    Interceptors can be declared inline to return a value or raise an exception
    when the declared exception is risen:

    >>> @intercept({
    ...    TypeError: returns('intercepted!')
    ... })
    ... def fails(foo):
    ...     if foo:
    ...         raise TypeError('inner exception')
    ...     return 'ok'
    >>> fails(False)
    'ok'
    >>> fails(True)
    'intercepted!'

    >>> @intercept({
    ...    TypeError: raises(Exception('intercepted!'))
    ... })
    ... def fail():
    ...     raise TypeError('inner exception')
    >>> fail()
    Traceback (most recent call last):
    ...
    Exception: intercepted!

    But they can also be declared and then used later on:

    >>> intercept0r = intercept({
    ...    TypeError: returns('intercepted!')
    ... })
    >>> @intercept0r
    ... def fail():
    ...     raise TypeError('raising error')
    >>> fail()
    'intercepted!'

    You can declare also an action that captures the risen exception by passing
    a callable to the action. This is useful to create a custom error message:

    >>> @intercept({
    ...    TypeError: returns(lambda e: 'intercepted {}'.format(e))
    ... })
    ... def fail():
    ...     raise TypeError('inner exception')
    >>> fail()
    'intercepted inner exception'

    Or to convert captured exceptions into custom errors:

    >>> class CustomError(Exception):
    ...     pass
    >>> @intercept({
    ...    TypeError: raises(lambda e: CustomError(e))
    ... })
    ... def fail():
    ...     raise TypeError('inner exception')
    >>> fail()
    Traceback (most recent call last):
    ...
    intercept.CustomError: inner exception

def intercept(actions: dict={}):
    """
    Decorates a function and handles any exceptions that may rise.

    Args:
        actions: A dictionary ``<exception type>: <action>``. Available actions\
            are :class:`raises` and :class:`returns`.

    Returns:
        Any value declared using a :class:`returns` action.

    Raises:
        AnyException: if AnyException is declared together with a
            :class:`raises` action.
        InterceptorError: if the decorator is called with something different
            from a :class:`returns` or :class:`raises` action.

    Interceptors can be declared inline to return a value or raise an exception
    when the declared exception is risen:

    >>> @intercept({
    ...    TypeError: returns('intercepted!')
    ... })
    ... def fails(foo):
    ...     if foo:
    ...         raise TypeError('inner exception')
    ...     return 'ok'
    >>> fails(False)
    'ok'
    >>> fails(True)
    'intercepted!'

    >>> @intercept({
    ...    TypeError: raises(Exception('intercepted!'))
    ... })
    ... def fail():
    ...     raise TypeError('inner exception')
    >>> fail()
    Traceback (most recent call last):
    ...
    Exception: intercepted!

    But they can also be declared and then used later on:

    >>> intercept0r = intercept({
    ...    TypeError: returns('intercepted!')
    ... })
    >>> @intercept0r
    ... def fail():
    ...     raise TypeError('raising error')
    >>> fail()
    'intercepted!'

    You can declare also an action that captures the risen exception by passing
    a callable to the action. This is useful to create a custom error message:

    >>> @intercept({
    ...    TypeError: returns(lambda e: 'intercepted {}'.format(e))
    ... })
    ... def fail():
    ...     raise TypeError('inner exception')
    >>> fail()
    'intercepted inner exception'

    Or to convert captured exceptions into custom errors:

    >>> class CustomError(Exception):
    ...     pass
    >>> @intercept({
    ...    TypeError: raises(lambda e: CustomError(e))
    ... })
    ... def fail():
    ...     raise TypeError('inner exception')
    >>> fail()
    Traceback (most recent call last):
    ...
    intercept.CustomError: inner exception
    """

    for action in actions.values():
        if type(action) is not returns and type(action) is not raises:
            raise InterceptorError('Actions must be declared as `returns` or `raises`')

    def decorated(f):
        def wrapped(*args, **kargs):
            try:
                return f(*args, **kargs)
            except Exception as e:
                if e.__class__ in actions:
                    return actions[e.__class__](e)
                else:
                    raise

        return wrapped

    return decorated