injector based dependency injection mechanism for nameko services. Project is similar to flask-injector.
Nameko provides a dependency injection mechanism, built-in in the framework. It works in many cases with the limitations:
- All the dependencies are injected regardless of whether they are used in the entry-point. For instance, all the dependencies will be injected for
/healthHTTP entry point. - Dependencies cannot depend on each other.
- The scope is an implementation detail. Frequency of the dependency creation depends on the
DependencyProviderimplementation. - Subjectively, implementing a new
DependencyProvideris not the easiest the task for the developers.
The library provides an alternative dependency injection mechanism to the one that is built-in in nameko. Several types of request scope can be used out of the box without special injector module declarations.
from nameko.containers.ServiceContainerfrom nameko.containers.WorkerContextwerkzeug.wrappers.Request, in case of HTTP requests
The library provides 2 scopes:
nameko_injector.core.request_scopewhere each request has own instance of the injected type.nameko_injected.core.resource_request_scopeit's likerequest_scopebut alsoclosemethod is called on each injected value after the request is processed to free the resources onDependencyProvider.worker_teardowncall.
An example of the test that declares service class and configuration provider:
import json
import typing as t
import injector
import pytest
from nameko.containers import ServiceContainer
from nameko.web.handlers import http
from nameko_injector.core import NamekoInjector
class ServiceConfig:
value: t.Mapping
@injector.provider
def provide_service_config(container: ServiceContainer) -> ServiceConfig:
return container.config
def configure(binder):
binder.bind(
ServiceConfig,
to=provide_service_config,
scope=injector.singleton,
)
INJECTOR = NamekoInjector(configure)
@INJECTOR.decorate_service
class Service:
name = "service-name"
@http("GET", "/config")
def view_config(self, request, config: ServiceConfig):
# 'config' is injected as singleton in each request that specifies it's type in
# the view function's signature.
return json.dumps(config)The library provides a plugin for pytest with some basic fixtures.
To enable the plugin, add the following line in your conftest.py module.
pytest_plugins = [
"nameko_injector.testing.pytest_fixtures",
]Graph of fixtures generated from the test
pytest --fixture-graph tests/test_http.py::test_http_request_injectedThere are several fixtures that help during the testing. All of the fixtures
have function pytest scope.
service_classfixture that MUST be redefined and return a service class under the test.web_servicefixture starts a real HTTP server to make real HTTP requests to the service. It can be used together with nameko's fixtureweb_sessionthat injects HTTP client that knows a correct port. Seetests/test_injected.pyas an example.injector_in_testfixture gives access to theinjector.Injectorinstance that will resolve the dependencies in the instance ofservice_class. The fixture uses a child injector from the one that decorates the service that provides isolation between the test cases with the same class under the test. By default, it usesworker_contextfixture.container_overridden_dependencies-web_serviceuses this mapping of nameko dependencies that need to be overridden with the instance values.worker_ctxfixture is used to getinjector_in_testvalue but it's a mock and might be redefined in your tests.
Let's assume that service depends on an HTTP client for some 3rd-party service.
In our test, we would like to use a mocked version of it. In that case, we need to
redefine injector_in_test fixture.
@pytest.fixture
def injector_in_test(injector_in_test, mocked_http_client):
injector_in_test.binder.bind(ThirdPartyServiceHttpClient, to=mocked_http_client)
# injector_in_test.binder.install(MockedClientModule())
return injector_in_testIn more sophisticated cases when we redefine how the server is started with
runner_factories main task is to ensure that the container (service instance
basically) has a valid injector. See
nameko_injector/testing/pytest_fixtures.py:web_service code as an example.
Main line there is replace_dependencies(container, **container_overridden_dependencies).
tox
