.NET Core Dependency Injection for Python
Project description
WD-DI: .NET Style Dependency Injection for Python
WD-DI is a lightweight dependency injection library for Python inspired by .NET's DI system. It provides a robust and flexible way to manage dependencies and lifetimes in your applications. For the python purists: WD-DI needs no external libraries, just python std libraries.
Check this README.md for an overview of patterns supported by WD-DI.
design_tutorial.md should provide some material for beginners on how to use DI to have a real positive impact on your software
todo_until_feature_complete.md lists todos needed to be done to be feature congruent to .NET
Features
WD-DI supports a variety of dependency injection patterns and configurations, including service lifetimes, constructor injection, configuration binding, middleware pipelines, and advanced lifetime management. Each feature comes with clear examples and guidance.
1. Service Lifetimes
What is it?
Service lifetimes define how instances of services are created and shared. WD-DI supports three lifetimes:
- Transient: A new instance is created every time the service is requested.
- Singleton: A single instance is created and shared across the entire application.
- Scoped: A single instance is created for a specific scope (e.g., per web request).
Example:
from wd_di import services
# Register services with explicit implementation types:
services.add_transient(IService, ServiceImpl) # Transient service
services.add_singleton(IService, ServiceImpl) # Singleton service
services.add_scoped(IService, ServiceImpl) # Scoped service
# Register service as self-implementing:
services.add_singleton(ServiceImpl)
# Using decorators for cleaner registration:
from wd_di.decorators import singleton, transient, scoped
@singleton()
class MyService:
def do_something(self):
pass
@transient()
class PerRequestService:
def do_something(self):
pass
@scoped()
class ScopedService:
def do_something(self):
pass
When to use:
- Use Transient for lightweight, stateless services.
- Use Singleton for services that should be shared and reused.
- Use Scoped for services tied to a specific context, such as a web request.
2. Dependency Injection (Constructor Injection)
What is it?
WD-DI uses constructor injection to automatically resolve and inject dependencies into your services. This promotes clear, testable, and maintainable code.
Example:
@singleton()
class UserService:
def __init__(self, db_service: DatabaseService):
self.db = db_service
def get_user(self, user_id: str):
return self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
# Build the service provider and resolve services:
provider = services.build_service_provider()
user_service = provider.get_service(UserService)
When to use:
Always prefer constructor injection—it makes dependencies explicit and simplifies unit testing.
3. Configuration and Options Pattern
What is it?
WD-DI provides a configuration system that binds configuration data to strongly-typed options classes. This pattern helps centralize configuration logic and provides type safety.
Example:
from dataclasses import dataclass
from wd_di import services
from wd_di.config import Configuration, Options
# Define an options class for your configuration
@dataclass
class DatabaseOptions:
connection_string: str = ""
max_connections: int = 10
# Create configuration from a dictionary (or JSON/environment variables)
config = Configuration({
"database": {
"connectionString": "mysql://localhost:3306/mydb",
"maxConnections": 100
}
})
# Register the configuration service
services.add_singleton_factory(IConfiguration, lambda _: config)
# Bind the configuration to strongly-typed options
services.configure(DatabaseOptions, section="database")
# Use the options in your service
@singleton()
class DatabaseService:
def __init__(self, options: Options[DatabaseOptions]):
self.connection_string = options.value.connection_string
self.max_connections = options.value.max_connections
When to use:
Use strongly-typed options to manage application settings and configurations in a clean, centralized way.
4. Middleware Pipeline
What is it?
The middleware pipeline allows you to compose processing logic in a sequence. This is ideal for cross-cutting concerns such as logging, authentication, validation, caching, and error handling.
Example:
from wd_di import services
from wd_di.middleware import IMiddleware, LoggingMiddleware, ValidationMiddleware
# Create custom middleware
class AuthMiddleware(IMiddleware):
async def invoke(self, context, next):
if not context.is_authenticated:
raise ValueError("Not authenticated")
return await next()
# Configure the middleware pipeline using a builder pattern:
app = services.create_application_builder()
app.configure_middleware(lambda builder: (
builder
.use_middleware(LoggingMiddleware)
.use_middleware(AuthMiddleware)
.use_middleware(ValidationMiddleware)
))
# Build the service provider and get the middleware pipeline:
provider = app.build()
pipeline = provider.get_service(MiddlewarePipeline)
# Execute the pipeline with a context object
result = await pipeline.execute(context)
Built-in Middleware Components:
- LoggingMiddleware: Logs pipeline execution.
- ExceptionHandlerMiddleware: Centralizes error handling.
- ValidationMiddleware: Validates the context.
- CachingMiddleware: Caches pipeline responses.
When to use:
Use middleware pipelines to decouple and modularize cross-cutting concerns in your processing flows.
5. Scoped Services
What is it?
Scoped services live only within a defined scope (e.g., a web request). WD-DI enforces explicit scope creation for such services and automatically disposes them when the scope ends.
Example:
provider = services.build_service_provider()
# Create a new scope
with provider.create_scope() as scope:
scoped_service = scope.get_service(MyService)
# Use the scoped service here
When to use:
Use scoped services when you need to manage the lifetime of resources such as database connections or transaction contexts, ensuring proper cleanup at the end of the scope.
Advanced Features
WD-DI also includes several advanced patterns that further extend its capabilities:
Instance Registration
What is it?
Register pre-created objects with the DI container. This is useful for sharing externally configured or created instances (like loggers or configuration objects).
Example:
class MyLogger:
def log(self, msg):
print(msg)
# Create and register an instance
logger_instance = MyLogger()
services.add_instance(MyLogger, logger_instance)
# Resolve and use the instance later:
provider = services.build_service_provider()
logger = provider.get_service(MyLogger)
assert logger is logger_instance # True
logger.log("Instance registration works!")
When to use:
Use instance registration when you need to inject a pre-configured or externally managed instance into your application.
Circular Dependency Detection
What is it?
Circular dependency detection safeguards your container against infinite recursion by detecting cycles in the dependency graph and raising a clear exception.
Example:
# Define services with a circular dependency
class ServiceA:
def __init__(self, service_b: "ServiceB"):
self.service_b = service_b
class ServiceB:
def __init__(self, service_a: ServiceA):
self.service_a = service_a
services.add_transient(ServiceA)
services.add_transient(ServiceB)
provider = services.build_service_provider()
try:
provider.get_service(ServiceA)
except Exception as e:
print(e) # Output includes: "Circular dependency detected for service: ..."
When to use:
This feature works automatically. It’s essential for catching configuration errors early during development when your dependency graph inadvertently contains cycles.
Explicit Scoped Services Management
What is it?
WD-DI enforces explicit scope creation and automatically disposes scoped services at the end of the scope. This ensures that resources are cleaned up properly.
Example:
# Define a disposable service
class DisposableResource:
def __init__(self):
self.disposed = False
def dispose(self):
self.disposed = True
services.add_scoped(DisposableResource)
provider = services.build_service_provider()
# Create a new scope and resolve a scoped service:
with provider.create_scope() as scope:
resource = scope.get_service(DisposableResource)
print(resource.disposed) # False; resource is active
# After the scope, the resource is automatically disposed:
print(resource.disposed) # True; dispose() was called
When to use:
Scoped management is ideal when your services hold resources that need cleanup, such as file handles, database connections, or network sockets.
Best Practices
-
Constructor Injection:
Always prefer constructor injection to clearly state dependencies and improve testability. -
Interface Segregation:
Register services against interfaces to allow flexible swapping and better test isolation. -
Strongly-Typed Configuration:
Use strongly-typed options for configuration to reduce runtime errors and centralize settings management. -
Middleware Separation:
Keep middleware focused on a single responsibility to ensure composability and maintainability.
Example Application
Below is a complete example that demonstrates how to set up and use WD-DI in a simple application:
from dataclasses import dataclass
from wd_di import services
from wd_di.config import Configuration, Options
# Define interfaces
class IUserRepository:
def get_user(self, user_id: str): pass
class IEmailService:
def send_email(self, to: str, subject: str, body: str): pass
# Define configuration for email
@dataclass
class EmailOptions:
smtp_server: str = ""
port: int = 587
username: str = ""
password: str = ""
# Implement services
@singleton()
class UserRepository(IUserRepository):
def get_user(self, user_id: str):
return {"id": user_id, "name": "John Doe", "email": "john@example.com"}
@singleton()
class EmailService(IEmailService):
def __init__(self, options: Options[EmailOptions]):
self.options = options.value
def send_email(self, to: str, subject: str, body: str):
print(f"Sending email via {self.options.smtp_server}")
# Email sending logic here
@singleton()
class UserService:
def __init__(self, repository: IUserRepository, email_service: IEmailService):
self.repository = repository
self.email_service = email_service
def notify_user(self, user_id: str, message: str):
user = self.repository.get_user(user_id)
self.email_service.send_email(
to=user["email"],
subject="Notification",
body=message
)
# Configure services and options
config = Configuration({
"email": {
"smtpServer": "smtp.gmail.com",
"port": 587,
"username": "myapp@gmail.com",
"password": "secret"
}
})
services.add_singleton_factory(IConfiguration, lambda _: config)
services.configure(EmailOptions, section="email")
services.add_singleton(IUserRepository, UserRepository)
services.add_singleton(IEmailService, EmailService)
# Build and use the service provider
provider = services.build_service_provider()
user_service = provider.get_service(UserService)
user_service.notify_user("123", "Hello, welcome to WD-DI!")
License
This project is licensed under the terms of the LICENSE file included in the repository.
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 wd_di-0.1.1.tar.gz.
File metadata
- Download URL: wd_di-0.1.1.tar.gz
- Upload date:
- Size: 36.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.5.26
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3df79c6f2ff62fc85706dfdef56ec0bd40dd71724363fe514288773dec724de3
|
|
| MD5 |
7079fc9f680ae7e34778794761bbef43
|
|
| BLAKE2b-256 |
3d9d555e117080da68df737898ad472b142ed1222201c26e5eab1b774069fcdc
|
File details
Details for the file wd_di-0.1.1-py3-none-any.whl.
File metadata
- Download URL: wd_di-0.1.1-py3-none-any.whl
- Upload date:
- Size: 12.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.5.26
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
99dc8ac907a6ef2bba0b432392647b4a29ec57803ea6760dca7b882ca642953a
|
|
| MD5 |
00d2af5c9449d245a8c7e446523aee55
|
|
| BLAKE2b-256 |
b0b26b7d61f70533ccb599fe18ccb9c07798c7b6532fc89954871b57459868c7
|