ets-labs/python-dependency-injector

How to use injector features in external libraries?

Opened this issue · 3 comments

In our project we have plenty of micro services(that are standalone git repositories). Some of them use dependency injector library to manage dependencies.

For some time ago we started using our library(that is internal for company, but external for every service).

We use external container to handle that, but for some reasons, external container, even got callable provider as dependency, but that callable miss any input parameter from origin config:

`
def _get_token(system_token: str):
print('default token in container _get_token callable provider:', system_token)
try:
token = context.token.get()
if token is None:
return system_token
return context.token.get()
except LookupError:
return system_token

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

token = providers.Callable(_get_token, config.system_token)

wiring_config = containers.WiringConfiguration(
    packages=(
        'src',
    ),
)

external_container = providers.Container(
    ExternalContainer,
    token=token,
    config=config,
)

`

I have created github repo, that show this problem https://github.com/Valt25/dependency_injector_issue_show_case.

There you can find several branches, they show different stages of project from naive usage, to container usage and the external library with container usage, and then my approach to fix it according to documentation.

Key changes is shown in README.md and you can see commits` diff.

So maybe I miss something in documentation, or misunderstand concept of dependent containers?

Few notes on external_library/external_service.py:

  1. Markers are resolved only if you decorate something with @inject.
  2. You're referring to the wrong container, it should be Provide[Container.external_container.token] (or Provide["external_container.token"]) to work. We do not support "relative" dependency resolution, unfortunatelly.

I guess you can see the issue. You have to structure your library code to not rely on @injects at all, only via explicitly providing them in the container itself. Please check #851 (comment), this should give you better idea how nested container should look like.

  1. If marker mean to be resolved, if according function parameter have some value in function instead of Provide object, then my example show that marker is resoved
  2. If I would use Container.external_container.token then it shows "type object 'ExternalContainer' has no attribute 'external_container'". Because ExternalContainer do not know which fields are in main container. I can not use main container here, because this is external library
  1. This is undocumented behavior. I wouldn't rely on it, even if it works somehow.
  2. That's right, you wire from your application's Container, not library one.

You have to be explicit about your dependencies in library-grade code. Here's how can you adjust the code tom make it work:

git diff
diff --git a/external_library/container.py b/external_library/container.py
index db51c73..85a52b2 100644
--- a/external_library/container.py
+++ b/external_library/container.py
@@ -1,10 +1,14 @@
+from functools import partial
+
 from dependency_injector import containers, providers
 
+from .external_service import get_data_from_external_service
+
 
 class Container(containers.DeclarativeContainer):
     token = providers.Dependency()
-    wiring_config = containers.WiringConfiguration(
-        packages=(
-            'external_library',
-        ),
+    get_data_from_external_service = providers.Factory(
+        partial,
+        get_data_from_external_service,
+        token=token,
     )
\ No newline at end of file
diff --git a/external_library/external_service.py b/external_library/external_service.py
index c1e35c4..a81892a 100644
--- a/external_library/external_service.py
+++ b/external_library/external_service.py
@@ -1,9 +1,4 @@
-from dependency_injector.wiring import Provide
-
-from external_library.container import Container
-
-
-def get_data_from_external_service(data: dict, token: str = Provide[Container.token]) -> dict:
+def get_data_from_external_service(data: dict, token: str) -> dict:
     print('token is used', token)
 
     return {
diff --git a/src/handler.py b/src/handler.py
index b891ae1..5ee3f63 100644
--- a/src/handler.py
+++ b/src/handler.py
@@ -1,8 +1,14 @@
-from external_library.external_service import get_data_from_external_service
+from dependency_injector.wiring import Provide, inject
+
 from src.common import Request, common_handler_decorator
+from src.container import Container
 
 
 @common_handler_decorator
-def handler(request: Request) -> dict:
+@inject
+def handler(
+    request: Request,
+    get_data_from_external_service = Provide[Container.external_container.get_data_from_external_service],
+) -> dict:
     result = get_data_from_external_service(request.data['external'])
     return result
\ No newline at end of file