python-dependency-injector icon indicating copy to clipboard operation
python-dependency-injector copied to clipboard

improve stack trace when object construction fails

Open laker-93 opened this issue 2 years ago • 1 comments

When the objects defined in the container cannot be constructed because the incorrect arguments have been passed, the stack trace that's produced when using the DI framework is not very clear. For example, modifying the example given in the docs to add an extra argument to ApiClient that is not passed to the construction of the object:

from unittest import mock
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide, inject


class ApiClient:
    def __init__(self, api_key: str, timeout: int, foo: str):
        self._api_key = api_key
        self._timeout = timeout


class Service:
    def __init__(self, api_client: ApiClient):
        self._api_client = api_client


class Container(containers.DeclarativeContainer):
    config = providers.Configuration()

    api_client = providers.Singleton(
        ApiClient,
        api_key=config.api_key,
        timeout=config.timeout,
    )

    service = providers.Factory(
        Service,
        api_client=api_client,
    )


@inject
def main(service: Service = Provide[Container.service]) -> None:
    pass


if __name__ == "__main__":
    container = Container()
    container.config.api_key.from_env("API_KEY", default='foo')
    container.config.timeout.from_env("TIMEOUT", as_=int, default=5)
    container.wire(modules=[__name__])

    main()  # <-- dependency is injected automatically

    with container.api_client.override(mock.Mock()):
        main()  # <-- overridden dependency is injected automatically

Produces a stack trace that's difficult to identify which object failed to be instantiated correctly:

Traceback (most recent call last):
  File "./di_test.py", line 43, in <module>
    main()  # <-- dependency is injected automatically
  File "src/dependency_injector/_cwiring.pyx", line 26, in dependency_injector._cwiring._get_sync_patched._patched
  File "src/dependency_injector/providers.pyx", line 225, in dependency_injector.providers.Provider.__call__
  File "src/dependency_injector/providers.pyx", line 2689, in dependency_injector.providers.Factory._provide
  File "src/dependency_injector/providers.pxd", line 650, in dependency_injector.providers.__factory_call
  File "src/dependency_injector/providers.pxd", line 577, in dependency_injector.providers.__call
  File "src/dependency_injector/providers.pxd", line 445, in dependency_injector.providers.__provide_keyword_args
  File "src/dependency_injector/providers.pxd", line 365, in dependency_injector.providers.__get_value
  File "src/dependency_injector/providers.pyx", line 225, in dependency_injector.providers.Provider.__call__
  File "src/dependency_injector/providers.pyx", line 3049, in dependency_injector.providers.Singleton._provide
  File "src/dependency_injector/providers.pxd", line 650, in dependency_injector.providers.__factory_call
  File "src/dependency_injector/providers.pxd", line 608, in dependency_injector.providers.__call
TypeError: __init__() missing 1 required positional argument: 'foo'

Compare this with the stack trace produced when constructing the object directly:

if __name__ == "__main__":
    api_client = ApiClient('foo', 5)

this gives a much clearer stack trace as to where the error is

File "./di_test.py", line 49, in <module>
    api_client = ApiClient('foo', 5)
TypeError: __init__() missing 1 required positional argument: 'foo'

It would be useful to at least print the object that failed to be constructed in the DI stack trace. When using the framework to constructed many applications across multiple containers, the issue of having to identify which object failed to be constructed becomes even more difficult.

laker-93 avatar Dec 19 '22 09:12 laker-93