import functools


def reraises(from_exc, to_exc, when=None, make_message=None):
    """Reraises exception thrown by decorated function.

    This decorator catches the specified exception 'from_exc' in the decorated function
    and reraises it as a different exception 'to_exc'.

    The exceptions to catch can further be limited by providing a predicate
    which should return 'True' for exceptions that should be reraised.

    It allows to optionally adjust the exception's message.

    Parameters
    ----------
    from_exc : type
        The original exception type
    to_exc : type
        The target exception type
    when : callable, optional
        A predicate that can be used to further refine
        the exceptions to be reraised.
    make_message : callable, optional
        Modify the original exception message.

    Examples
    --------

    Reraise ValueError as TypeError with a new message:

    >>> @reraises(
    ...     from_exc=ValueError,
    ...     to_exc=TypeError,
    ...     make_message=lambda m: m + " (I used to be a ValueError)"
    ... )
    ... def foo(e):
    ...     raise(e)

    >>> foo(ValueError("Error!"))
    Traceback (most recent call last):
        ...
    TypeError: Error! (I used to be a ValueError)

    Other exceptions are unaffected!

    >>> foo(RuntimeError("Error!"))
    Traceback (most recent call last):
        ...
    RuntimeError: Error!
    """

    def decorator(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            try:
                return f(*args, **kwargs)
            except from_exc as e:
                if when is None or when(e):
                    msg = str(e) if make_message is None else make_message(str(e))
                    raise to_exc(msg) from e
                else:
                    raise e

        return wrapper

    return decorator