Migration Guide: Upgrading to modern-di 1.x¶
!!! warning "Historical guide"
This guide covers migrating from 0.x to 1.x. The APIs shown here (AsyncContainer, SyncContainer, providers.Singleton, .cast) were removed in 2.x.
If you are on 1.x today, also follow the 2.x migration guide to reach the current API.
This document describes the changes required to migrate from modern-di 0.x versions to modern-di 1.x.
Overview¶
The migration to modern-di 1.x involves several key changes in the API, including: - Updated import paths and class names - Changes in container initialization and usage - Modified provider resolution methods
Key Changes¶
1. Import Path Changes¶
The following import paths and class names have changed:
BaseGraph→GroupContainer→AsyncContainerorSyncContainer(depending on your needs)
For example:
# Before (0.x)
from modern_di import BaseGraph, Container
# After (1.x)
from modern_di import Group, AsyncContainer, SyncContainer
2. Provider Changes¶
Several provider types were added, removed, or changed:
New Providers¶
- ContextProvider: A new provider type that allows injecting context values into dependencies. This is particularly useful for injecting framework-specific objects like requests, websockets, etc.
Removed Providers¶
- Selector: The
Selectorprovider has been removed. Its functionality can be replicated usingFactoryproviders combined withContextProvider. - ContextAdapter: The
ContextAdapterprovider has been removed and replaced withContextProvider. Its functionality can also be replicated usingFactoryproviders combined withContextProvider. - AttrGetter: The
AttrGetterprovider has been removed. If you were using it to access attributes of other providers, you should refactor your code to use direct provider references or factory functions instead.
Removed Functionality¶
- Factory Provider Attributes: The ability to inject factory functions themselves (using
.async_providerand.sync_providerattributes) has been removed. - If you were injecting factory functions, you should refactor your code to inject DI-container itself and resolve dependencies manually in-place.
Using ContextProvider with Factory as a Replacement¶
The combination of ContextProvider and Factory can replicate the functionality of both the old Selector and ContextAdapter providers:
# Before (0.x) - Using Selector
def fetch_db_mode() -> str:
# Some logic to determine which database to use
return "write" if some_condition else "read"
dynamic_engine = providers.Selector(
Scope.REQUEST,
fetch_db_mode,
write=database_engine,
read=database_replica_engine,
)
# After (1.x) - Using ContextProvider with Factory for Selector replacement
# First, define a ContextProvider for any context you need
some_context = providers.ContextProvider(Scope.REQUEST, SomeContextType)
# Then use a Factory that takes the context and makes decisions
dynamic_engine = providers.Factory(
Scope.REQUEST,
choose_database_engine, # A function that decides which engine to use
context=some_context.cast, # Pass the context to the factory function
database_engine=database_engine.cast,
database_replica_engine=database_replica_engine.cast,
)
# The factory function would look like:
def choose_database_engine(
context: SomeContextType,
database_engine: DatabaseEngine,
database_replica_engine: DatabaseReplicaEngine,
) -> DatabaseEngine:
# Replicate the selector logic here
return database_engine if should_use_write_db(context) else database_replica_engine
3. Container Initialization¶
The single Container class has been split into two separate classes: AsyncContainer and SyncContainer.
However, it's important to note that AsyncContainer can still be used to synchronously resolve providers.
Before (0.x):
# Synchronous container
container = Container()
container.sync_enter()
# Asynchronous container
container = Container()
container.async_enter()
After (1.x):
# Synchronous container
sync_container = SyncContainer(groups=ALL_GROUPS)
sync_container.enter()
# Asynchronous container (can still resolve providers synchronously)
async_container = AsyncContainer(groups=ALL_GROUPS)
async_container.enter()
# Synchronously resolve a provider with AsyncContainer
instance = async_container.sync_resolve_provider(provider)
# Asynchronously resolve a provider with AsyncContainer
instance = await async_container.resolve_provider(provider)
The key difference is that AsyncContainer supports both synchronous and asynchronous provider resolution, while SyncContainer only supports synchronous resolution.
Choose AsyncContainer if you need to resolve both synchronous and asynchronous providers, or SyncContainer if you only need synchronous resolution.
4. Provider Resolution¶
The method for resolving providers has changed significantly. The dependency resolution pattern has been inverted - instead of calling resolution methods on the provider, you now call them on the container. This provides a more consistent and intuitive API.
Before (0.x):
# Resolving a provider directly - called on the provider
instance = provider.sync_resolve(container)
instance = await provider.async_resolve(container)
# Resolving creators from a graph - called on the provider
await ioc.UseCases.create_chat_use_case.async_resolve(container)
After (1.x):
# Resolving a provider directly - called on the container
instance = container.sync_resolve_provider(provider)
instance = await container.resolve_provider(provider)
# Resolving by type - called on the container
instance = container.sync_resolve(SomeType)
instance = await container.resolve(SomeType)
The key changes are:
1. Inverted dependency: Resolution is now called on the container instead of the provider
2. Type-based resolution: You can now resolve dependencies directly by type, not just by provider reference
3. Consistent naming: sync_resolve_provider() and resolve_provider() for provider-based resolution, sync_resolve() and resolve() for type-based resolution
5. Resolution by Dependency Type¶
One of the key new features in modern-di 1.x is the ability to resolve dependencies by type rather than by provider reference. This provides a more intuitive and flexible way to access dependencies.
To enable this feature, you must pass the groups parameter when initializing your container:
# Initialize container with groups to enable type-based resolution
container = AsyncContainer(groups=ALL_GROUPS)
container.enter()
# Now you can resolve dependencies by type
instance = container.sync_resolve(SomeType)
instance = await container.resolve(SomeType)
# You can still resolve by provider reference if needed
instance = container.sync_resolve_provider(some_provider)
instance = await container.resolve_provider(some_provider)
The groups parameter is a list of Group classes that contain your providers. Each provider in these groups is registered with its return type, allowing the container to look up providers by type:
from modern_di import Group, Scope, providers
class DatabaseGroup(Group):
database_engine = providers.Singleton(Scope.APP, DatabaseEngine)
repository = providers.Factory(Scope.REQUEST, Repository, db_engine=database_engine.cast)
# Register the group when creating the container
ALL_GROUPS = [DatabaseGroup]
container = AsyncContainer(groups=ALL_GROUPS)
# Now you can resolve by type
db_engine = container.sync_resolve(DatabaseEngine)
repository = await container.resolve(Repository)
This feature simplifies dependency access and makes the API more intuitive, as you no longer need to maintain references to specific providers throughout your codebase.
Migration Steps¶
- Update Dependencies: Ensure all modern-di packages are updated to 1.x versions
- Update Import Statements: Replace old import paths with new ones (
BaseGraph→Group,Container→AsyncContainer/SyncContainer) - Update Provider Definitions: Replace
SelectorandContextAdapterproviders withFactoryandContextProvidercombinations where needed - Remove AttrGetter and Factory Attribute Usage: Replace any
AttrGetterproviders and remove usage of.async_providerand.sync_providerattributes - Update Container Initialization: Use
AsyncContainer(groups=ALL_GROUPS)orSyncContainer(groups=ALL_GROUPS)instead ofContainer()(note thatAsyncContainersupports both synchronous and asynchronous provider resolution) - Update Container Methods: Replace
async_enter()withenter()andsync_resolve()withsync_resolve_provider() - Update Provider Resolution: Replace
provider.resolve(container)withcontainer.resolve_provider(provider)orcontainer.resolve(Type)(resolution is now called on the container, not the provider)
Breaking Changes¶
BaseGraphclass renamed toGroupContainerclass replaced withAsyncContainerandSyncContainerasync_enter()method replaced withenter()sync_resolve()method on providers replaced withsync_resolve_provider()on containersasync_resolve()method on providers replaced withresolve_provider()on containersSelectorprovider type removed (useFactorywithContextProviderinstead)ContextAdapterprovider type removed (useContextProviderinstead)AttrGetterprovider type removed- Factory provider attributes (
.async_providerand.sync_provider) removed - Manual provider overrides are handled differently
- The way dependencies are declared in web framework applications has changed
- Container initialization now requires passing groups explicitly to enable type-based resolution
- Dependency resolution is now called on containers instead of providers