Skip to main content

Dependency Injection

Framework M uses a Dependency Injection (DI) system powered by the dependency-injector library. This container acts as the Universal Composer, providing a unified orchestration layer that remains independent of any specific execution context (API, CLI, Jobs, or REPL).

This guide explains the "Lifecycle of a Dependency"—from defining a service to advanced runtime overrides.

1. Implement Your Service

A service is a Python class that performs business logic. To support different deployment modes (like Indie vs Enterprise), it is best practice to define a Protocol for your service and use Constructor Injection.

# my_app/interfaces.py
from typing import Protocol

class WeatherAdapter(Protocol):
async def get_temp(self, city: str) -> float: ...

# my_app/services/weather_service.py
class WeatherService:
def __init__(self, adapter: WeatherAdapter) -> None:
# Standard Constructor Injection
self.adapter = adapter

async def show_forecast(self, city: str):
temp = await self.adapter.get_temp(city)
return f"It is {temp}°C in {city}"

2. Create an App Container

Register your service and its dependencies in a DeclarativeContainer. This is where you wire your protocols to concrete implementations.

# my_app/container.py
from framework_m_core.di import containers, providers
from .services.weather_service import WeatherService
from .adapters.local_weather import LocalWeatherAdapter

class MyAppContainer(containers.DeclarativeContainer):
# 1. Define the adapter (can be overridden later)
weather_adapter = providers.Singleton(LocalWeatherAdapter)

# 2. Inject the adapter into the service via constructor
weather_service = providers.Singleton(
WeatherService,
adapter=weather_adapter
)

3. Injecting Dependencies

There are two primary ways to access your services depending on the context.

Option A: Constructor Injection (Service-to-Service)

This is the standard pattern for most application logic. As shown in Phase 2, the DI container automatically handles passing dependencies into constructors when it instantiates your services.

Option B: Function Injection (Entry Points)

For "top-level" code like CLI commands or API controllers, use the @inject decorator and the Provide marker.

from framework_m_core.di import inject, Provide
from framework_m_core.container import Container

@inject
async def get_weather_cmd(
city: str,
# Access via Container.<app_name>.<service_name>
weather: WeatherService = Provide[Container.my_app.weather_service]
):
print(await weather.show_forecast(city))

4. Advanced: Late-Binding & Protocol Overrides

One of Framework M's most powerful features is Progressive Decomposition. You can swap a local service for a remote microservice without changing your application code.

In your Bootstrap Step, you can override providers based on environment variables or deployment modes:

# my_app/bootstrap.py
class WeatherBootstrap(BootstrapProtocol):
order: int = 40

def run(self, container: Any) -> None:
# The framework's hydrate_apps() orchestrator automatically
# calls auto_load_app_containers() before this run() executes.

if os.environ.get("USE_CLOUDS"):
from .adapters.cloud_weather import CloudWeatherAdapter
# Override the local adapter with a cloud implementation
container.my_app.weather_adapter.override(
providers.Singleton(CloudWeatherAdapter)
)

5. Controller & Lifecycle Hook Patterns

DocType Controllers (and some short-lived objects) are instantiated dynamically by the framework and do not support constructor injection. Additionally, they often risk Circular Dependencies if they import containers at the top level.

To handle DI in these cases, use the Service Locator pattern with a local import:

# my_app/doctypes/customer/controller.py
class CustomerController(BaseController):
async def before_save(self, context=None):
# 1. Use a local import inside the method to avoid circular loops
from my_app.container import MyAppContainer

# 2. Access the service directly from the internal container
svc = MyAppContainer.weather_service()
await svc.do_something(self.doc)

6. Registration Summary

Framework M uses Entry Points for discovery:

  1. Register Container: In pyproject.toml under framework_m.containers.
  2. Register Bootstrap: In pyproject.toml under framework_m.bootstrap.
  3. Automatic Discovery: The framework automatically discovers and hydrates your app container via the framework_m.containers entry point during startup. No manual activation is required.

[!TIP] Use the command m entrypoints to verify that your container and service overrides have been correctly discovered by the framework.