ivankorobkov / python-inject

Python dependency injection
Apache License 2.0
671 stars 77 forks source link

Django configuration #79

Open dineshtrivedi opened 3 years ago

dineshtrivedi commented 3 years ago

Apologies if this is not the right way of asking questions. Please let me know If I should ask somewhere else.

I was wondering if there is any documentation explaining the best place to use inject with a Django application. For instance, should I used the AppConfig.ready method to centralize injection per Django app? Is there any other guideline or recommendation?

Thank you very much.

ivankorobkov commented 3 years ago

Hi!

I'm not an expert at Django. Unfortunately, I do not know whether you should use AppConfig.ready or not.

unrealsolver commented 3 years ago

I also use AppConfig.ready, but now I don't know how to override one specific binding in the test (without cleaning all bindings).

dineshtrivedi commented 3 years ago

@unrealsolver I wrote the documentation below for my team (with more real examples in our case), and we have been using it and so far it has been working. Any feedback is very much appreciated. I am also cleaning all the binds so I don't know if it can help you.

The convention is the following:

class PolicyConfig(AppConfig): name = 'policy' verbose_name = 'Policy'

def ready(self):
    # pylint: disable=import-outside-toplevel
    from policy.inject import policy_app_inject_config
    inject.configure_once(policy_app_inject_config)
* Injecting in a class, we use the method `inject.attr` as a [descriptor](https://docs.python.org/3/howto/descriptor.html) instead of `inject.instace` which returns instance straight away. service is declared as a descriptor in the example below

class CustomerGetAndUpdateView(GetAndUpdate): service = inject.attr(CustomerService)

* Every Django app that requires DI has two files of configuration called **inject.py** and **tests/inject.py**
* **inject.py**: It includes a method with all the mapping and necessary configuration for the DI of the Django app in Question

from inject import Binder

from commons.dto.dto_svc import DtoMapper from policy.repository import PolicyRepository from policy.service import PolicyService, AnotherPolicyService, AnotherService

all = ['policy_app_inject_config']

def policy_app_inject_config(binder: Binder): binder.bind(PolicyRepository, PolicyRepository()) binder.bind(DtoMapper, DtoMapper()) binder.bind(PolicyService, PolicyService()) binder.bind(AnotherPolicyService, AnotherPolicyService()) binder.bind(AnotherService, AnotherService())

* **tests/inject.py**: Includes the **inject.py** plus the necessary DI configuration for the tests

from inject import Binder

from client.tests.fixtures import ClientFixture from policy.inject import policy_app_inject_config

all = ['policy_app_test_inject_config']

def policy_app_test_inject_config(binder: Binder): binder.install(policy_app_inject_config) binder.bind(ClientFixture, ClientFixture())

* Being a [descriptor](https://docs.python.org/3/howto/descriptor.html) is what allows the override of a previously bound configuration to have an effect on the tests.  It is important to remember that you can change the injection for the tests that you want to. Just remember to configure the inject in your setUp method and clear the configuration in the tearDown method

class ClientPolicyTest(LumkaniTestCase): def setUp(self): inject.clear_and_configure(policy_app_test_inject_config) ...

def tearDown(self):
    inject.clear()
    ...

def test_clients_with_policy_should_like_potato(self):
    # Setup Mock
    customer_service_mock = MagicMock(spec_set=CustomerService)
    inject.clear_and_configure(lambda binder: binder.bind(CustomerService, customer_service_mock))
    ...
tandrieuxx commented 8 months ago

I also used AppConfig.ready to configure injections for a django app along with inject.configure(config, once=True), but when I tried to use inject on a second app, I noticed that one configuration was not taken into account. Only one app could be working at once, while accessing the other raised the error "Could not instanciate abstract class with abstract methods [...]" which means injection conf was not found by inject. It was kind of random which app would be working and which would not be. Browsing the code I figured that using configure_once or configure(once=True) means that only the first call does something (kind of obvious), but once=False makes the second call raise an error. The only other way is configure(clear=True) but I don't want to clear, I'd like to keep and add.

So either there was never a way to configure many apps separately and @dineshtrivedi 's answer is surprising because it seems to be working for them, or the behavior of inject changed in some version but I couldn't find the info.

Anyway as a workaround, I define all my injections in settings.py and call inject.configure(settings.INJECTIONS, once=True) in every django app that needs them since Django does not allow inject.configure to be called in the settings, because apps are not loaded yet.