def retry(limit=1):
    """
    Retry a failing function `n` times or until it executes successfully. If the function does not complete
    successfully by the nth retry, then the exception will be reraised for the executing code to handle.

    Example:

    ::

        root@710027b06106:/plsnocrash/examples# cat retry.py
        import plsnocrash

        fail_counter = 0

        @plsnocrash.retry(5)
        def get_data():
            global fail_counter
            # Fail three times before completing
            if fail_counter < 3:
                fail_counter += 1
                raise ValueError("Something went wrong")
            return "some data"


        if __name__ == '__main__':
            print(get_data())
        root@710027b06106:/plsnocrash/examples# python retry.py
        Caught exception: Something went wrong, retry 1/5
        Caught exception: Something went wrong, retry 2/5
        Caught exception: Something went wrong, retry 3/5
        some data


    :param f: Function to wrap
    :param limit: int, number of retries. If None then retry forever. Default 1.
    :type limit: int
    :return: Wrapped function
    """

    # Implementation notes:
    # Three ways this could be called as a decorator:
    # @retry -- Gets used as a raw decorator, limit is actually a function, this is handled below
    # @retry() -- default value on limit actually gets used, limit is set to 1
    # @retry(value) or @retry(limit=value) -- value is assigned to limit

    def retry_decorator(f):
        if not (limit is None or (isinstance(limit, int) and limit >= 0)):
            raise ValueError('Invalid repeat limit', limit)
        if limit is None:
            def inf_wrapper(*args, **kwargs):
                i = 1
                while True:
                    try:
                        return f(*args, **kwargs)
                    except Exception as e:
                        print("Caught exception {}, retry {:d}/inf".format(e, i))
                        i += 1
                        continue
            return inf_wrapper
        else:
            def wrapper(*args, **kwargs):
                for i in range(limit+1):
                    try:
                        return f(*args, **kwargs)
                    except Exception as e:
                        print("Caught exception: {}, retry {:d}/{:d}".format(e, i+1, limit))
                        # Reraise exception if does not succeed by final retry
                        if i == limit:
                            raise e

            return wrapper
    # if used as a decorator without an argument, default to 1
    if callable(limit):
        f = limit
        limit = 1
        return retry_decorator(f)
    return retry_decorator