Dependency Injection¶
Power of Dependency Injection
Unchained provides a robust dependency injection system built on FastDepends, enabling clean, testable, and modular code with full type safety.
What is Dependency Injection?¶
Dependency injection is a design pattern that allows you to:
- Separate the creation of dependencies from their usage
- Improve testability by making dependencies replaceable
- Create reusable components that can be shared across your application
- Keep your code modular and maintainable
In Unchained, dependency injection is implemented using Python's Annotated
type and the Depends
function.
Basic Usage¶
Important: Using Annotated
Unchained's dependency injection system requires the use of Python's Annotated
type. Direct parameter typing without Annotated
will not work for dependencies.
Automatic Dependencies¶
Unchained provides several built-in dependencies that are automatically injected when you type-annotate your parameters:
from unchained import Unchained
from unchained.settings.base import UnchainedSettings
# Or your custom settings
from myapp.settings import AppSettings
app = Unchained()
@app.get("/settings-info")
def get_settings_info(settings: UnchainedSettings): # Or settings: AppSettings
return {
"debug": settings.debug,
"database_url": settings.database_url
}
For more information about state management, see the state management documentation.
Type Aliases for Dependencies¶
You can create clear and reusable type aliases for your dependencies, which improves code organization and readability:
from typing import Annotated
from unchained import Depends, Unchained
from httpx import AsyncClient
app = Unchained()
def get_api_client() -> AsyncClient:
return AsyncClient(base_url="https://api.example.com")
# Create a type alias for the dependency
ApiClient = Annotated[AsyncClient, Depends(get_api_client)]
@app.get("/external-data")
def get_external_data(client: ApiClient):
# Use the client with full IDE completion
response = client.get("/data")
return response.json()
This pattern is particularly useful for:
- Improving code readability
- Enabling better IDE support
- Reusing dependencies across multiple endpoints
- Making the dependency relationship clear
Nested Dependencies¶
Dependencies can depend on other dependencies, creating a dependency graph:
from typing import Annotated
from unchained import Depends, Unchained
app = Unchained()
def get_api_key():
return "api-key-1234"
def get_headers(api_key: Annotated[str, Depends(get_api_key)]):
return {"Authorization": f"Bearer {api_key}"}
def get_client(headers: Annotated[dict, Depends(get_headers)]):
# In a real app, you might return httpx.AsyncClient or similar
return {"headers": headers, "client": "api_client"}
@app.get("/api-data")
def get_data(client: Annotated[dict, Depends(get_client)]):
# Access the fully resolved dependency chain
return {"data": "some data", "client": client}
When a request is made to /api-data
:
get_api_key()
is called first- Its return value is passed to
get_headers()
- The result from
get_headers()
is passed toget_client()
- Finally, the
get_client()
result is injected as theclient
parameter
Advanced Examples with Custom State and Settings¶
Here are simpler examples showing how to leverage custom state and settings:
Custom State Example¶
from typing import Annotated, Optional
from unchained import Depends, Unchained
from unchained.states import BaseState
from httpx import AsyncClient
# Define custom state
class AppState(BaseState):
weather_api_key: str = "default-key"
logger_enabled: bool = True
# Get configuration from state
def get_api_config(state: AppState):
return {
"api_key": state.weather_api_key,
"logging": state.logger_enabled
}
# Use the configuration
def get_weather_info(config: Annotated[dict, Depends(get_api_config)]):
# Simple function returning weather info based on config
return {
"source": "Weather API",
"using_key": config["api_key"],
"logging_enabled": config["logging"]
}
# Create app with state
app = Unchained(state=AppState())
# Use in endpoint
@app.get("/weather-config")
def weather_config(info: Annotated[dict, Depends(get_weather_info)]):
return info
Custom Settings Example¶
from typing import Annotated
from unchained import Depends, Unchained
from unchained.settings.base import UnchainedSettings
from pydantic import Field
# Define custom settings
class AppSettings(UnchainedSettings):
# API Settings
api_timeout: int = Field(default=30, ge=1, le=120)
enable_cache: bool = Field(default=True)
# Simple feature flag checker
def is_feature_enabled(feature_name: str, settings: AppSettings):
if feature_name == "cache":
return settings.enable_cache
return False
# Get timeout configuration
def get_timeout_config(settings: AppSettings):
return {
"timeout": settings.api_timeout,
"cache_enabled": settings.enable_cache
}
# Create app
app = Unchained()
# Use settings in endpoint
@app.get("/api-config")
def api_config(
config: Annotated[dict, Depends(get_timeout_config)],
cache_enabled: Annotated[bool, Depends(lambda s: is_feature_enabled("cache", s))]
):
return {
"config": config,
"cache_status": "enabled" if cache_enabled else "disabled"
}
Combining Custom State and Settings¶
from typing import Annotated
from unchained import Depends, Unchained
from unchained.states import BaseState
from unchained.settings.base import UnchainedSettings
from pydantic import Field
# Custom settings
class ApiSettings(UnchainedSettings):
base_url: str = Field(default="https://api.example.com")
timeout: int = Field(default=30)
# Custom state
class AppState(BaseState):
api_key: str = "default-key"
debug_mode: bool = False
# Initialize app
app = Unchained(state=AppState())
# Function that uses both state and settings
def get_api_configuration(state: AppState, settings: ApiSettings):
return {
"base_url": settings.base_url,
"timeout": settings.timeout,
"api_key": state.api_key,
"debug": state.debug_mode
}
# Use the configuration
@app.get("/api-status")
def api_status(config: Annotated[dict, Depends(get_api_configuration)]):
return {
"status": "configured",
"config": config
}
Best Practices¶
-
Use
Annotated
for All Dependencies: Always useAnnotated
withDepends
for proper type inference. -
Keep Dependencies Focused: Each dependency should have a single responsibility.
-
Prefer Function Dependencies: Function-based dependencies are easier to test and mock.
-
Cache When Appropriate: Use
use_cache=True
(the default) to avoid recalculating the same dependency. -
Handle Errors Properly: Dependencies should raise appropriate exceptions for error scenarios.
Implementation Details¶
Under the hood, Unchained uses FastDepends to handle dependency resolution. The process works as follows:
- When a route is registered, Unchained analyzes its signature
- For each parameter with
Annotated[Type, Depends(...)]
, it registers the dependency - Automatic dependencies like
Request
,BaseUnchained
,BaseState
, andUnchainedSettings
are detected by type - When a request arrives, dependencies are resolved in the correct order
- The resolved values are passed to the route handler