Simplified implementation of Staged Dependency Injection from Scala's distage library of Izumi Project
Project description
Chibi Izumi
A Python re-implementation of some core concepts from Scala's Izumi Project,
distage staged dependency injection library in particular.
The port was done by guiding Claude with thorough manual reviews.
At this point the project is not battle-tested. Expect dragons, landmines and varying mileage.
Features
distage provides a powerful, type-safe dependency injection framework with:
- Non-invasive design - No decorators, base classes, or framework-specific code required in your business logic
- Fluent DSL for defining bindings - Type-safe API with
.using().value()/.type()/.func()/.factory() - Signature introspection - Automatic extraction of dependency requirements from type hints
- Dependency graph formation and validation - Build and validate the complete dependency graph at startup
- Automatic logger injection - Seamless injection of location-based loggers without manual setup
- Factory bindings - Create new instances on-demand with assisted injection (
Factory[T]) - Named dependencies - Distinguished dependencies using
@Idannotations - Roots for dependency tracing - Specify what components should be instantiated
- Activations for configuration - Choose between alternative implementations using configuration axes
- Garbage collection - Only instantiate components reachable from roots
- Circular dependency detection - Early detection of circular dependencies
- Missing dependency detection - Ensure all required dependencies are available
- Tagged bindings - Support for multiple implementations of the same interface
- Set bindings - Collect multiple implementations into sets
Quick Start
from izumi.distage import ModuleDef, Injector, PlannerInput
# Define your classes
class Database:
def __init__(self, connection_string: str):
self.connection_string = connection_string
def query(self, sql: str) -> str:
return f"DB[{self.connection_string}]: {sql}"
class UserService:
def __init__(self, database: Database):
self.database = database
def create_user(self, name: str) -> str:
return self.database.query(f"INSERT INTO users (name) VALUES ('{name}')")
# Configure bindings using the new fluent API
module = ModuleDef()
module.make(str).using().value("postgresql://prod:5432/app")
module.make(Database).using().type(Database) # Constructor injection
module.make(UserService).using().type(UserService)
# Create injector and get service
injector = Injector()
planner_input = PlannerInput([module])
user_service = injector.get(planner_input, UserService)
# Use the service
result = user_service.create_user("alice")
print(result) # DB[postgresql://prod:5432/app]: INSERT INTO users (name) VALUES ('alice')
Core Concepts
ModuleDef - Binding Definition DSL
The ModuleDef class provides a fluent DSL for defining dependency bindings using the new algebraic data structure approach:
from izumi.distage import ModuleDef, Factory
# Example classes for demonstration
class Config:
def __init__(self, debug: bool = False, db_url: str = ""):
self.debug = debug
self.db_url = db_url
class Database:
def __init__(self, connection_string: str):
self.connection_string = connection_string
class PostgresDatabase(Database):
def __init__(self, connection_string: str):
super().__init__(connection_string)
class UserService:
def __init__(self, database: Database):
self.database = database
class Handler:
def handle(self):
pass
class UserHandler(Handler):
def handle(self):
return "user"
class AdminHandler(Handler):
def handle(self):
return "admin"
# Now define the bindings
module = ModuleDef()
# Instance binding
module.make(Config).using().value(Config(debug=True))
# Class binding (constructor injection)
module.make(Database).using().type(PostgresDatabase)
# Factory function binding
def create_database(config: Config) -> Database:
return Database(config.db_url)
module.make(Database).named("custom").using().func(create_database)
# Factory bindings for non-singleton semantics
module.make(Factory[UserService]).using().factory(UserService)
# Named bindings for multiple instances
module.make(str).named("db-url").using().value("postgresql://prod:5432/app")
module.make(str).named("api-key").using().value("secret-key-123")
# Set bindings for collecting multiple implementations
module.many(Handler).add_type(UserHandler)
module.many(Handler).add_type(AdminHandler)
Automatic Logger Injection
Chibi Izumi automatically provides loggers for dependencies without names, creating location-specific logger instances:
import logging
from izumi.distage import ModuleDef, Injector, PlannerInput
class Database:
def __init__(self, connection_string: str):
self.connection_string = connection_string
def query(self, sql: str) -> str:
return f"DB[{self.connection_string}]: {sql}"
class UserService:
# Logger automatically injected based on class location
def __init__(self, database: Database, logger: logging.Logger):
self.database = database
self.logger = logger # Will be logging.getLogger("__main__.UserService")
def create_user(self, name: str) -> str:
self.logger.info(f"Creating user: {name}")
return self.database.query(f"INSERT INTO users (name) VALUES ('{name}')")
# No need to configure loggers - they're injected automatically!
module = ModuleDef()
module.make(str).using().value("postgresql://prod:5432/app")
module.make(Database).using().type(Database)
module.make(UserService).using().type(UserService)
injector = Injector()
planner_input = PlannerInput([module])
user_service = injector.get(planner_input, UserService)
Factory Bindings for Non-Singleton Semantics
Use Factory[T] when you need to create multiple instances with assisted injection:
from typing import Annotated
from izumi.distage import Factory, Id, ModuleDef, Injector, PlannerInput
class Database:
def __init__(self, connection_string: str):
self.connection_string = connection_string
class UserSession:
def __init__(self, database: Database, user_id: str, api_key: Annotated[str, Id("api-key")]):
self.database = database
self.user_id = user_id
self.api_key = api_key
module = ModuleDef()
module.make(str).using().value("postgresql://prod:5432/app")
module.make(Database).using().type(Database)
module.make(Factory[UserSession]).using().factory(UserSession)
injector = Injector()
planner_input = PlannerInput([module])
factory = injector.get(planner_input, Factory[UserSession])
# Create new instances with runtime parameters
session1 = factory.create("user123", **{"api-key": "secret1"})
session2 = factory.create("user456", **{"api-key": "secret2"})
# Database is injected from DI, user_id and api_key are provided at creation time
Named Dependencies with @Id
Use @Id annotations to distinguish between multiple bindings of the same type:
from typing import Annotated
from izumi.distage import Id, ModuleDef, Injector, PlannerInput
class DatabaseService:
def __init__(
self,
primary_db: Annotated[str, Id("primary-db")],
replica_db: Annotated[str, Id("replica-db")]
):
self.primary_db = primary_db
self.replica_db = replica_db
module = ModuleDef()
module.make(str).named("primary-db").using().value("postgresql://primary:5432/app")
module.make(str).named("replica-db").using().value("postgresql://replica:5432/app")
module.make(DatabaseService).using().type(DatabaseService)
injector = Injector()
planner_input = PlannerInput([module])
db_service = injector.get(planner_input, DatabaseService)
Dependency Graph Validation
The dependency graph is built and validated when creating a plan:
from izumi.distage import ModuleDef, Injector, PlannerInput
class A:
def __init__(self, b: "B"):
self.b = b
class B:
def __init__(self, a: A):
self.a = a
# This will detect circular dependencies
module = ModuleDef()
module.make(A).using().type(A)
module.make(B).using().type(B)
try:
injector = Injector()
planner_input = PlannerInput([module])
plan = injector.plan(planner_input) # Validation happens here
print("This should not print - circular dependency should be caught")
except Exception as e:
# Catches circular dependencies, missing dependencies, etc.
pass # Expected to happen
Set Bindings
Collect multiple implementations into a set:
from izumi.distage import ModuleDef, Injector, PlannerInput
class CommandHandler:
def handle(self, cmd: str) -> str:
pass
class UserHandler(CommandHandler):
def handle(self, cmd: str) -> str:
return f"User: {cmd}"
class AdminHandler(CommandHandler):
def handle(self, cmd: str) -> str:
return f"Admin: {cmd}"
class CommandProcessor:
def __init__(self, handlers: set[CommandHandler]):
self.handlers = handlers
module = ModuleDef()
module.many(CommandHandler).add_type(UserHandler)
module.many(CommandHandler).add_type(AdminHandler)
module.make(CommandProcessor).using().type(CommandProcessor)
injector = Injector()
planner_input = PlannerInput([module])
processor = injector.get(planner_input, CommandProcessor)
# processor.handlers contains instances of both UserHandler and AdminHandler
Advanced Usage Patterns
Multiple Execution Patterns
from izumi.distage import ModuleDef, Injector, PlannerInput
class Config:
def __init__(self, default_user: str = "test"):
self.default_user = default_user
class UserService:
def __init__(self, config: Config):
self.config = config
def create_user(self, name: str) -> str:
return f"Created user: {name}"
module = ModuleDef()
module.make(Config).using().type(Config)
module.make(UserService).using().type(UserService)
injector = Injector()
planner_input = PlannerInput([module])
# Pattern 1: Plan + Locator (most control)
plan = injector.plan(planner_input)
locator = injector.produce(plan)
service = locator.get(UserService)
# Pattern 2: Function injection (recommended)
def business_logic(service: UserService, config: Config) -> str:
return service.create_user(config.default_user)
result = injector.produce_run(planner_input, business_logic)
# Pattern 3: Simple get (for quick usage)
service = injector.get(planner_input, UserService)
Architecture
Chibi Izumi follows these design principles from the original distage:
- Non-invasive design - Your classes remain framework-free, just use regular constructors
- Compile-time safety - Dependencies are validated at plan creation time
- Type-safe bindings - Algebraic data structure ensures binding correctness
- Immutable bindings - Bindings are defined once and cannot be modified
- Explicit dependency graph - All dependencies are explicit and traceable
- Fail-fast validation - Circular and missing dependencies are detected early
- Zero-configuration features - Automatic logger injection, factory patterns
Limitations
This is a working implementation with some simplifications compared to the full distage library:
- Forward references in type hints have limited support
- No advanced lifecycle management (startup/shutdown hooks)
- Simplified error messages compared to Scala version
- No compile-time dependency graph visualization tools
Contributing
This project was developed through AI-assisted programming with thorough manual review. Contributions, bug reports, and feedback are welcome!
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file chibi_izumi-0.1.0.tar.gz.
File metadata
- Download URL: chibi_izumi-0.1.0.tar.gz
- Upload date:
- Size: 29.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e1f8b8770c97443a144609b6a8d0b95bcb93692b86febc03010aa566cc7835ab
|
|
| MD5 |
659555c5a9b96364ea18b08680e234c3
|
|
| BLAKE2b-256 |
efab19f852f4d8471f7e9e096a3a871b22c18b00300c8297c6321dc835304584
|
File details
Details for the file chibi_izumi-0.1.0-py3-none-any.whl.
File metadata
- Download URL: chibi_izumi-0.1.0-py3-none-any.whl
- Upload date:
- Size: 32.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a1690686e9462bce1de111a7619a884e9c8c1c2bd964405974afc93c86ea5e32
|
|
| MD5 |
d974a1ee6b2e63a95092b7e799b16737
|
|
| BLAKE2b-256 |
9bbaa89c206db25fce116317c7967f48130ffbdcd64732aa81ae5ae2286a5594
|