ets-labs / python-dependency-injector

Dependency injection framework for Python
https://python-dependency-injector.ets-labs.org/
BSD 3-Clause "New" or "Revised" License
3.89k stars 304 forks source link

Pytest hangs when trying to initialize a container with async resources #698

Open Cito opened 1 year ago

Cito commented 1 year ago

When trying to create a container in a pytest fixture with pytest-asyncio, I had the effect that pytest ran forever with no error message showing up.

Here is a simplified version of the code that procuced the problem:

from dependency_injector import containers, providers
from pytest import mark

def factory(uses=None, name=None):
    return name

async def init_async_resource(uses=None, name=None):
    yield name

class Container(containers.DeclarativeContainer):
    resource1 = providers.Resource(init_async_resource, name="resource1")
    factory1 = providers.Factory(factory, uses=resource1, mame="factory1")
    resource2 = providers.Resource(init_async_resource, uses=factory1, name="resource2")

@mark.asyncio
async def test_container():
    container = Container()
    await container.init_resources()
    assert await container.resource2() == "resource2"

Notice there is an error in line 2 of the container, it should be name instead of mame. When the misspelled parameter is corrected, the test runs through flawlessly. But if you run pytest on this code with the typo, it hangs forever without revealing the actual error.

Tested with: Python 3.11, pytest 7.3.1, pytest-asyncio 0.21.0, dependency-injector 4.41.0

Note that this only happens when you run the test with pytest-asyncio. When you run it with asyncio directly, it does not hang and produces the expected error.

So, the problem might be on the side of pytest-asyncio here. But maybe something should also be done on the side of dependency-injector to mightigate such problems, because they are huge time-wasters and hard to debug. Leaving it also here for others who might experience similar problems.

Cito commented 1 year ago

Here is a possible test file for this issue.

import asyncio
from dependency_injector import containers, providers
from pytest import mark, raises

def factory(uses):
    """A synchronous factory function."""
    return uses

async def init_async_resource(uses):
    """An asynchronous resource generator."""
    yield uses

class GoodContainer(containers.DeclarativeContainer):
    """A container that should work."""

    resource1 = providers.Resource(init_async_resource, uses="something")
    factory1 = providers.Factory(factory, uses=resource1)
    resource2 = providers.Resource(init_async_resource, uses=factory1)

class BadContainer(containers.DeclarativeContainer):
    """A container with a bad parameter."""

    resource1 = providers.Resource(init_async_resource, uses="something")
    factory1 = providers.Factory(factory, uses=resource1, arg="bad")
    resource2 = providers.Resource(init_async_resource, uses=factory1)

@mark.asyncio
async def test_good_container():
    container = GoodContainer()
    await asyncio.wait_for(container.init_resources(), timeout=5)
    assert await container.resource2() == "something"

@mark.asyncio
async def test_bad_container():
    container = BadContainer()
    with raises(TypeError):  # raises a TimeoutError instead
        await asyncio.wait_for(container.init_resources(), timeout=5)