python-injector / injector

Python dependency injection framework, inspired by Guice
BSD 3-Clause "New" or "Revised" License
1.29k stars 81 forks source link

Binding multiple interfaces to same implementation #206

Open qb-fredrikomstedt opened 1 year ago

qb-fredrikomstedt commented 1 year ago

Hi!

This issue is somewhat related to #181. Let's say I have the following code structure:

from abc import ABC, abstractmethod

class FileReader(ABC):
    @abstractmethod
    def read(self, file_path: str) -> str:
        pass

class FileWriter(ABC):
    @abstractmethod
    def write(self, file_path: str) -> None:
        pass

class FileHandler(FileReader, FileWriter):
    def read(self, file_path: str) -> str:
        # Do file reading stuff

    def write(self, file_path: str) -> None:
        # Do file writing stuff

Essentially a class that implements two interfaces, where only one interface may be needed elsewhere at a given time (for instance, many classes may need to read the files but only a few may need to write to them).

The FileHandler class could be instantiated as a Singleton, which in turn makes it reasonable that both FileReader and FileWriter bind to the same object. As is mentioned in #181, the following does not work (it creates two instances of FileHandler):

injector = Injector()

injector.binder.bind(FileReader, to=FileHandler, scope=singleton)
injector.binder.bind(FileWriter, to=FileHandler, scope=singleton)

Instead, it is suggested to create a Module doing the following:

class FileModule(injector.Module):
    def configure(self, binder: injector.Binder) -> None:
        binder.bind(FileHandler, scope=singleton)

    @provider
    def provide_reader(self, implementation: FileHandler) -> FileReader:
        return implementation

    @provider
    def provide_writer(self, implementation: FileHandler) -> FileWriter:
        return implementation

This seems like a lot of code for a binding of several interfaces to one instance. In other DI frameworks, I've seen syntax similar to:

injector.binder.bind([FileReader, FileWriter], to=FileHandler, scope=singleton)

I don't know if I've missed something, but is this possible to do in injector? If not, I think it would be a nice addition to the library.

Thanks!

qb-fredrikomstedt commented 1 year ago

I've managed to solve this on my end by adding a bind_several function to the Binder class. I don't know if it covers all use cases of how injector can be used, but it does cover mine, and allows me to bind multiple interfaces to one implementation (and one instance of that implementation if the request_scope is singleton). It basically uses the same functionality as the Module classes do, just wrapped into a function.

Someone more experienced with this library could potentially point out if this is a good addition to this repository, if it requires some modification, or if it doesn't belong here at all. Regardless, the problem is solved on my end. :)

def bind_several(
    self,
    interfaces: List[Type[T]],
    implementation_type: Type[T],
    scope: Union[None, Type[Scope], ScopeDecorator] = None,
):
    def get_implementation(implementation: implementation_type):  # type: ignore
        return implementation

    self.bind(implementation_type, scope=scope)
    for interface in interfaces:
        self.bind(interface, to=inject(get_implementation), scope=scope)

As an example, it can be used like this:

class C(A, B):
    ...

injector.binder.bind_several([A, B], C, scope=singleton)
jstasiak commented 1 year ago

This would be nice to have I think