Transform a function with breakpoints (aka generator) into a function.

    The result of `function` can (and often will) be used as a decorator.

    Arguments
    ---------

      - `on_yield`: function handler factory.

        A call `handler = on_yield()` is performed every time the
        decorated generator is called. The object `handler` shall be 
        function-like ; it is called for every yield of the generator
        function, with the following keyword arguments:

          - `progress`: a floating-point number between `0.0` (when the
            function execution starts) and `1.0` (when it ends), or `None`
            when this information is not available.

          - `elapsed`: the time elapsed since the function call in seconds.

          - `remaining`: the estimated time to the end of the function, 
             in seconds, or `None` when no estimate is available.

          - `args`: positional arguments of the decorated generator.

          - `kwargs`: keyword arguments of the decorated generator.  

          - `result`: partial result of the decorated generator.

        If the handler returns a value that is not `None`, the decorated
        generator execution stops and this value is returned.

      - `progress`: `True` when the generator yields `(progress, result)`
        instead of `result`.

      - `dt` is the target time between two successive breakpoints, 
        or `None` if there is no such target.
 
        At every breakpoint, the generator receives a floating-point number;
        it is a multiplier that should be applied to the current period between
        breakpoints to reach the `dt` target.
        The multiplier may be `None` if no estimate is available.

    Returns
    -------

      - `decorator`: a decorator for generator functions.

def function(on_yield=None, progress=False, dt=None):
    """
    Transform a function with breakpoints (aka generator) into a function.

    The result of `function` can (and often will) be used as a decorator.

    Arguments
    ---------

      - `on_yield`: function handler factory.

        A call `handler = on_yield()` is performed every time the
        decorated generator is called. The object `handler` shall be 
        function-like ; it is called for every yield of the generator
        function, with the following keyword arguments:

          - `progress`: a floating-point number between `0.0` (when the
            function execution starts) and `1.0` (when it ends), or `None`
            when this information is not available.

          - `elapsed`: the time elapsed since the function call in seconds.

          - `remaining`: the estimated time to the end of the function, 
             in seconds, or `None` when no estimate is available.

          - `args`: positional arguments of the decorated generator.

          - `kwargs`: keyword arguments of the decorated generator.  

          - `result`: partial result of the decorated generator.

        If the handler returns a value that is not `None`, the decorated
        generator execution stops and this value is returned.

      - `progress`: `True` when the generator yields `(progress, result)`
        instead of `result`.

      - `dt` is the target time between two successive breakpoints, 
        or `None` if there is no such target.
 
        At every breakpoint, the generator receives a floating-point number;
        it is a multiplier that should be applied to the current period between
        breakpoints to reach the `dt` target.
        The multiplier may be `None` if no estimate is available.

    Returns
    -------

      - `decorator`: a decorator for generator functions.

    """
    if dt is not None:
        dt = float(dt)
        if dt <= 0.0:
          error = "dt={0} is invalid, use a positive number (or None)."
          raise ValueError(error.format(dt))

    def decorator(generator_function):
        def function_(*args, **kwargs):
            if on_yield is not None:
                yield_handler = on_yield()
            else:
                yield_handler = None

            generator = generator_function(*args, **kwargs)
            t0 = t = None
            multiplier = None
            while True:
                try:
                    info = generator.send(multiplier)
                    if progress:
                        progress_, result = info
                    else:
                        progress_, result = None, info

                    rt = float("nan")
                    if t0 is None: # first yield
                        t0 = t = time.time()
                    else:
                        t_ = time.time()
                        dt_ = t_ - t
                        t = t_
                        if dt:
                            try:
                                multiplier = dt / dt_
                            except ZeroDivisionError: # unspecified behavior
                                multiplier = float("inf")
                        if progress_ is not None:
                            q = (1.0 - progress_) * (t - t0)
                            try:
                                rt = q / progress_
                            except ZeroDivisionError: # unspecified behavior
                                if q != 0.0:
                                    sign = (q > 0.0) - (q < 0.0) 
                                    rt = sign * float("inf")
                                # otherwise, rt = 0.0 / 0.0, and the default
                                # (rt = nan) is fine.
                    if yield_handler:
                        handler_result = yield_handler(progress=progress_, 
                                                       elapsed=t-t0, 
                                                       remaining=rt, 
                                                       result=result,
                                                       # not documented:
                                                       args=args,     
                                                       kwargs=kwargs)

                        if handler_result is not None:
                            return handler_result # not documented.
                            # The case where we want to stop the execution
                            # *and* return None is not supported.
                except StopIteration:
                    return result
        function_.decorator = decorator
        function_.generator = generator_function
        return function_
    return decorator