Skip to content

[BUG] Exits stacks can not be unwound with exception details #255

Description

@agriffin-grow

Describe the bug
When an exception is raised with a normal (Async)ExitStack used as a context manager (see fastapi), __aexit__ is called with exception details. fastapi_injectable has no way to mirror this behavior, always forcing us to use an ugly workaround (see below)

To Reproduce

from collections.abc import AsyncGenerator
from typing import Annotated

from fastapi import Depends
from fastapi_injectable import cleanup_all_exit_stacks, injectable


async def _get_dbconnection() -> AsyncGenerator[object, None]:
    conn = object()
    try:
        print("yield connection")
        yield conn
    except Exception:
        print("conn.rollback()")
        raise
    else:
        print("conn.commit()")
    finally:
        print("conn.close()")


@injectable()
async def as_dependency(connection: Annotated[object, Depends(_get_dbconnection)]):
    raise Exception("boom")


async def main():
    try:
        await as_dependency()
    except Exception:
        # BUG HERE
        # in the face of an exception, we should be able to call __aexit__ with exception
        # information so that we can properly unwind the exit stack and call rollback() on the
        # dependency
        print("got exception")
    finally:
        await cleanup_all_exit_stacks()


if __name__ == "__main__":
    import asyncio

    asyncio.run(main())

Expected behavior
I want to be able to unwind an exit stack with an error as would happen if it were used as a context manager.

My suggestion is to add an exc: Exception parameter to the _close_stack method such that it calls

if exc:
    await stack.__aexit__(type(exc), exc, exc.__traceback__)
else:
    await stack.aclose()

and plumb it all the way through from the close_exit_stacks methods

Ugly workaround
Looks something like this...

async def _injectable_wrapper(callable):
    scope = InjectableScope()
    await scope.__aenter__()
    try:
        result = callable(*args, **kwargs)
        if asyncio.iscoroutine(result):
            result = await result
        await scope.__aexit__(None, None, None)
        return result
    except Exception as exc:
        await scope.__aexit__(type(exc), exc, exc.__traceback__)
        raise

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions