Allows using a generator to chain together reversible actions.

    This function is very similar to :py:func:`reversible.gen` except that it
    may be used with actions whose ``forwards`` and/or ``backwards`` methods
    are couroutines. Specifically, if either of those methods return futures
    the generated action will stop execution until the result of the future is
    available.

    .. code-block:: python

        @reversible.tornado.gen
        @tornado.gen.coroutine
        def save_comment(ctx, comment):
            ctx['comment_id'] = yield async_http_client.fetch(
                # ...
            )
            raise tornado.gen.Return(ctx['comment_id'])

        @save_comment.backwards
        def delete_comment(ctx, comment):
            # returns a Future
            return async_http_client.fetch(...)

        @reversible.tornado.gen
        def post_comment(post, comment, client):
            try:
                comment_id = yield save_comment(comment)
            except CommentStoreException:
                # Exceptions thrown by actions may be caught by the
                # action.
                yield queue_save_comment_request(comment)
            else:
                yield update_comment_count(post)
                update_cache()

    :param function:
        The generator function. This generator must yield action objects. The
        ``forwards`` and/or ``backwards`` methods on the action may be
        asynchronous operations returning coroutines.
    :param io_loop:
        IOLoop used to execute asynchronous operations. Defaults to the
        current IOLoop if omitted.
    :returns:
        An action executable via :py:func:`reversible.tornado.execute` and
        yieldable in other instances of :py:func:`reversible.tornado.gen`.

def gen(function, io_loop=None):
    """Allows using a generator to chain together reversible actions.

    This function is very similar to :py:func:`reversible.gen` except that it
    may be used with actions whose ``forwards`` and/or ``backwards`` methods
    are couroutines. Specifically, if either of those methods return futures
    the generated action will stop execution until the result of the future is
    available.

    .. code-block:: python

        @reversible.tornado.gen
        @tornado.gen.coroutine
        def save_comment(ctx, comment):
            ctx['comment_id'] = yield async_http_client.fetch(
                # ...
            )
            raise tornado.gen.Return(ctx['comment_id'])

        @save_comment.backwards
        def delete_comment(ctx, comment):
            # returns a Future
            return async_http_client.fetch(...)

        @reversible.tornado.gen
        def post_comment(post, comment, client):
            try:
                comment_id = yield save_comment(comment)
            except CommentStoreException:
                # Exceptions thrown by actions may be caught by the
                # action.
                yield queue_save_comment_request(comment)
            else:
                yield update_comment_count(post)
                update_cache()

    :param function:
        The generator function. This generator must yield action objects. The
        ``forwards`` and/or ``backwards`` methods on the action may be
        asynchronous operations returning coroutines.
    :param io_loop:
        IOLoop used to execute asynchronous operations. Defaults to the
        current IOLoop if omitted.
    :returns:
        An action executable via :py:func:`reversible.tornado.execute` and
        yieldable in other instances of :py:func:`reversible.tornado.gen`.
    """

    @functools.wraps(function)  # TODO: use wrapt instead?
    def new_function(*args, **kwargs):
        try:
            value = function(*args, **kwargs)
        except _RETURNS as result:
            return SimpleAction(
                lambda ctx: ctx.value,
                lambda _: None,
                result,
            )
        else:
            if isinstance(value, types.GeneratorType):
                return _TornadoGeneratorAction(value, io_loop)
            else:
                return SimpleAction(
                    lambda _: value,
                    lambda _: None,
                    None,
                )

    return new_function