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

Type-based dependency resolution for wiring? #637

Open agargenta opened 1 year ago

agargenta commented 1 year ago

First of all, thank you for this this amazing project!

I'm not sure if this has come up before, but have you considered adding support for type-based wiring identifiers to simplify the dependency resolution?

Looking at your Django example, instead of:

from django.http import HttpRequest, HttpResponse
from dependency_injector.wiring import inject, Provide
from githubnavigator.containers import Container
from githubnavigator.services import SearchService

@inject
def index(
        request: HttpRequest,
        search_service: SearchService = Provide[Container.search_service],
        …,
) -> HttpResponse: …

it would be great if we could write it as:

from django.http import HttpRequest, HttpResponse
from dependency_injector.wiring import inject, Provide
from githubnavigator.services import SearchService

@inject
def index(
        request: HttpRequest,
        search_service: SearchService = Provide[SearchService],
        …,
) -> HttpResponse: …

By not importing the githubnavigator.containers.Container, we would minimize the risk of issues such as https://github.com/ets-labs/python-dependency-injector/issues/466, where the container is initialized before Django's apps are fully loaded.

Additionally, the application code should ideally not depend on (i.e. know about) the container in general, and how its resources (i.e. .search_service) are managed in particular. Using string-identifiers helps, but those are more brittle (e.g. during refactoring). Of course, string-identifiers would still be valuable for resolving dependencies without unique types, but, if you exclude "config.…", those are more of an exception than the rule.

Type-based dependency resolution is rather common in the Java world with its @Inject annotation (and an occasional @Named to resolve multiple deps with the same type), so I was curious as to why it's not available here.

To be clear, I am only suggesting this be available for the top-level resources (such as the SearchSevice above), and not any nested resources. One way this resolution could work is as follows:

import inspect
from typing import Optional, Type
from dependency_injector.containers import Container
from dependency_injector.providers import Factory, Provider, Singleton

def resolve_provider_by_type(container: Container, cls: Type) -> Optional[Provider]:
  providers = inspect.getmembers(container, lambda p : isinstance(p, (Factory, Singleton)) and issubclass(p.cls, cls))
  if providers:
    if len(providers) > 1:
      raise ValueError(f"Cannot uniquely resolve {cls}. Found {len(providers)} matching resources.")
    return providers[0][1]
  return None

Thank you for your consideration.

StummeJ commented 1 year ago

I like this as well. One reason we're using this library over others is complex situations where you have multiple instances of a single type. I think having multiple ways would be great though.