Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Question] What does the @inject decorator do? #404

Closed
ecs-jnguyen opened this issue Feb 23, 2021 · 8 comments
Closed

[Question] What does the @inject decorator do? #404

ecs-jnguyen opened this issue Feb 23, 2021 · 8 comments
Assignees
Labels

Comments

@ecs-jnguyen
Copy link

Hi I am playing around with the dependency_injector library and I am trying to find out what the @inject decorator does. I have tried this sample program with and without the @inject decorator and noticed no difference. Is the @inject decorate necessary?

import sys
import uuid

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


class Service:
    def __init__(self):
        self.uuid = uuid.uuid4()


class Container(containers.DeclarativeContainer):

    service = providers.Factory(Service)


@inject # Commenting this out has no effect on the program
def main(service: Service = Provide[Container.service]) -> None:
    print(service.uuid)


if __name__ == '__main__':
    container = Container()
    container.wire(modules=[sys.modules[__name__]])

    main()
@rmk135 rmk135 self-assigned this Feb 24, 2021
@rmk135
Copy link
Member

rmk135 commented Feb 24, 2021

Hi @ecs-jnguyen ,

That's a very good question. My pleasure to provide an explanation.

Decorator @inject registers functions and methods in wiring module.

Why that's needed? The answer is in how wiring works. Wiring introspects a module and looks for any functions or methods that have Provide markers. That works fine, but there are some cases when it's impossible to introspect properly.

Case 1 is closures:

def foo():
    @inject
    def bar(x = Provide[...]):  # no way to reach it with introspection
        ...

Case 2 is replacing class decorators. The example of this is click library. Its decorator is class-based. This class based decorator hides the actual callable in the internals. No guarantee that it can be introspected:

@click.command()
@click.option('-c', '--configuration-file', help='Configuration file path', default='config.yml')
@click.argument('test_key')
@inject  # Registers it in wiring module before the reference is masked by click class
def cli(test_key, configuration_file, config: Config = Provider[Container.config]) -> None:
    ...

print(type(cli))  # Output: <class 'click.core.Command'>

Case 3 is memorizing decorator. This is typical for web and api frameworks when defining the routes. Framework decorator memorizes a reference to the function before the wiring happens. Even when wiring passes later, other framework keeps reference to pre-wiring-decorated callable:

from fastapi import APIRouter, Depends

router = APIRouter()


@router.get('/', response_model=Response)
@inject  # Registers it in wiring module before router has memorized pre-decorated function
async def index(
        query: Optional[str] = None,
        limit: Optional[str] = None,
        default_query: str = Depends(Provide[Container.config.default.query]),
        default_limit: int = Depends(Provide[Container.config.default.limit.as_int()]),
        search_service: SearchService = Depends(Provide[Container.search_service]),
):
    ...

Early versions of the Dependency Injector 4 didn't have @inject decorator. It worked fine when framework could introspect. I've added @inject after discovered cases above. To not confuse people with the rules, I recommend to use @inject all the time. It gives a guarantee that wiring will find the markers.

@ecs-jnguyen
Copy link
Author

@rmk135 Thank you for answering my question! I appreciate the time and effort you guys put into all this!

@rmk135
Copy link
Member

rmk135 commented Feb 24, 2021

No problems, thanks for asking.

PS: When you say "you guys" it's actually only me for now.

@rmk135 rmk135 closed this as completed Feb 24, 2021
@ecs-jnguyen
Copy link
Author

ecs-jnguyen commented Feb 24, 2021

@rmk135 Sorry to message you after you closed the issue. I have 1 more quick question.

I'm trying to find a way to use the Provide[...] as much as possible so that there is less setup for each application. I saw that in your I saw in your application example that none of the classes in example/services.py are using Provide[...] for the constructor parameters and you are wiring everything inside the Application container.

Do you recommend initializing multiple containers and calling the container.wire(module=[...] method like I have done below?

import sys

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

import uuid


# This would be in ConfigContainer.py
class ConfigContainer(containers.DeclarativeContainer):
    config = providers.Configuration(default={'key': 'value'})


# This would be in ClientA.py
class ClientA:
    def __init__(self, config: dict = Provide[ConfigContainer.config]):
        self.uuid = uuid.uuid4()
        self.config = config


# This would be in ClientA.py
class ClientsContainer(containers.DeclarativeContainer):
    services = providers.Singleton(ClientA)


# This would be in ServiceA.py
class ServiceA:
    @inject
    def __init__(self, client_a: ClientA = Provide[ClientsContainer.services]):
        self.client_a = client_a

    def print(self):
        print(self.client_a.uuid)
        print(self.client_a.config)


def main() -> None:
    config_container = ConfigContainer()
    config_container.config.from_dict({'key': 'otherValue'})
    config_container.wire(modules=[sys.modules[__name__]])

    client_container = ClientsContainer()
    client_container.wire(modules=[sys.modules[__name__]])

    a = ServiceA()
    a.print()


if __name__ == '__main__':
    main()

EDIT: I removed the ApplicationContainer class

@rmk135 rmk135 reopened this Feb 24, 2021
@ecs-jnguyen
Copy link
Author

Btw I edited the code. I just realized in my example the ApplicationContainer class does nothing in this case.

@rmk135
Copy link
Member

rmk135 commented Feb 24, 2021

Do you recommend initializing multiple containers and calling the container.wire(module=[...] method like I have done below?

There's nothing bad in it, but I would recommend to gather everything in a single container. Wiring feature role is to ease the integration with other frameworks. Your own code typically could not use wiring. The idea is that you gather everything in the container and then use wiring to inject your components to the framework of your choice. That was an initial idea, but that's not a mandatory rule. You could have multiple containers and wire them to the any part of the code without any limitations.

Btw, in case you face circular imports problem you can use wiring with string identifiers https://python-dependency-injector.ets-labs.org/wiring.html#strings-identifiers

@ecs-jnguyen
Copy link
Author

@rmk135 Thanks for getting back to me. using the 'string' identifiers should help. Hopefully i don't run into any circular dependencies lol

@rmk135
Copy link
Member

rmk135 commented Feb 24, 2021

I'm closing this issue. Open a new one if any help needed.

@rmk135 rmk135 closed this as completed Feb 24, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants