Lightweight type-hint based Dependency Injection container with auto-wiring for Python
Project description
💉 miraveja-di
A lightweight, type-hint based Dependency Injection container for Python with auto-wiring capabilities
Etymology: Combining "dependency injection" with the Miraveja ecosystem naming convention
🚀 Overview
miraveja-di is a modern dependency injection container that leverages Python's type hints for automatic dependency resolution. Built with DDD/Hexagonal Architecture principles, it provides a clean, testable way to manage dependencies in your applications.
Part of the Miraveja ecosystem, miraveja-di provides dependency management infrastructure for all ecosystem services.
✨ Key Features
- 🔍 Auto-wiring - Automatically resolve dependencies using constructor type hints
- ⏱️ Multiple Lifetimes - Support for Singleton, Transient, and Scoped lifetimes
- 📦 Batch Registration - Register multiple dependencies at once using dictionaries
- 🔄 Circular Dependency Detection - Detect and report circular dependencies with full chain
- ⚡ FastAPI Integration - First-class support for FastAPI with
Depends()helpers - 🧪 Testing Utilities - Built-in mocking and override capabilities for testing
- 🏗️ Clean Architecture - Organized following DDD/Hexagonal Architecture principles
🛠️ Technology Stack
🐍 Core Runtime
- Python 3.10+ - Type hints and modern Python features
- typing-extensions - Compatibility for Python 3.8-3.9
🌐 Optional Integrations
- FastAPI - Web framework integration
- pytest - Testing framework support
🧪 Development
- pytest - Testing framework with async support
- pytest-asyncio - Async testing utilities
- pytest-cov - Coverage reporting
- black - Code formatter
- pylint - Code quality checker
- isort - Import statement organizer
- pre-commit - Git hook framework for automated checks
🏛️ Architecture
miraveja-di follows Domain-Driven Design and Hexagonal Architecture principles:
src/miraveja_di/
├── 🧠 domain/ # Core business logic (models, enums, interfaces, exceptions)
├── 🎬 application/ # Use cases (container, resolver, lifetime manager)
└── 🔌 infrastructure/ # External integrations (FastAPI, testing utilities)
Dependency Rule: Domain ← Application ← Infrastructure
- Domain has no dependencies on other layers
- Application depends only on Domain
- Infrastructure depends on Application and Domain
🎯 Getting Started
📋 Prerequisites
- Python 3.10+
- Poetry 2.0+ (recommended) or pip
🚀 Installation
poetry add miraveja-di
Or with pip:
pip install miraveja-di
For FastAPI integration:
poetry add miraveja-di[fastapi]
📖 Quick Start
Basic Usage
from miraveja_di import DIContainer, Lifetime
# Initialize container
container = DIContainer()
# Register dependencies
container.register_singletons({
DatabaseConfig: lambda c: DatabaseConfig.from_env(),
DatabaseConnection: lambda c: DatabaseConnection(c.resolve(DatabaseConfig)),
})
container.register_transients({
RequestHandler: lambda c: RequestHandler(c.resolve(DatabaseConnection)),
})
# Register scoped dependencies (per-request state)
container.register_scoped({
RequestContext: lambda c: RequestContext(),
})
# Resolve dependencies (auto-wiring)
handler = container.resolve(RequestHandler)
# Use scoped container for request-specific dependencies
with container.create_scope() as scoped:
ctx = scoped.resolve(RequestContext)
# Same instance within this scope
ctx2 = scoped.resolve(RequestContext)
assert ctx is ctx2
Auto-Wiring
The container automatically resolves constructor dependencies using type hints:
class UserService:
def __init__(self, db: DatabaseConnection, logger: Logger):
self.db = db
self.logger = logger
# Only register what can't be auto-wired
container.register_singletons({
DatabaseConnection: lambda c: DatabaseConnection("postgresql://..."),
Logger: lambda c: Logger("app.log"),
})
# UserService will be auto-wired
user_service = container.resolve(UserService)
⏱️ Lifetime Management
Singleton
Single instance shared across the entire application:
container.register_singletons({
AppConfig: lambda c: AppConfig.from_env(),
CacheService: lambda c: CacheService(c.resolve(AppConfig)),
})
Transient
New instance created every time it's resolved:
container.register_transients({
RequestProcessor: lambda c: RequestProcessor(),
EventHandler: lambda c: EventHandler(c.resolve(EventBus)),
})
Scoped
Single instance per scope (e.g., per HTTP request):
# Register scoped dependencies
container.register_scoped({
RequestContext: lambda c: RequestContext(),
RequestLogger: lambda c: RequestLogger(c.resolve(RequestContext)),
})
# Create a scope using context manager
with container.create_scope() as scoped_container:
# Same instance within this scope
service1 = scoped_container.resolve(RequestContext)
service2 = scoped_container.resolve(RequestContext)
assert service1 is service2
# Scoped instances automatically cleaned up after exiting the context
Lifetime Comparison Table
| Lifetime | Instance Count | Shared Across | Use Case | Registration Method |
|---|---|---|---|---|
| Singleton | One per application | Entire application | Configuration, database connections, caches | register_singletons() |
| Transient | New every time | Not shared | Lightweight, stateless operations | register_transients() |
| Scoped | One per scope | Within scope (e.g., HTTP request) | Request-specific state, transactions | register_scoped() |
Key Behaviors:
- Singleton: Instances created once and cached for the lifetime of the application
- Transient: New instance created every time
resolve()is called - Scoped: Instances cached within a scope, shared by all resolutions in that scope, cleaned up when scope exits
⚡ FastAPI Integration
Basic Integration
from fastapi import FastAPI, Depends
from miraveja_di import DIContainer
from miraveja_di.infrastructure.fastapi_integration import create_fastapi_dependency
app = FastAPI()
container = DIContainer()
# Register dependencies
container.register_singletons({
DatabaseConnection: lambda c: DatabaseConnection(),
UserRepository: lambda c: UserRepository(c.resolve(DatabaseConnection)),
UserService: lambda c: UserService(c.resolve(UserRepository)),
})
# Create FastAPI dependency
get_user_service = create_fastapi_dependency(container, UserService)
@app.get("/users/{user_id}")
async def get_user(
user_id: int,
user_service: UserService = Depends(get_user_service)
):
return await user_service.get_user(user_id)
Scoped Dependencies per Request
Use middleware to create a scoped container for each HTTP request:
from miraveja_di.infrastructure.fastapi_integration import (
ScopedContainerMiddleware,
create_scoped_dependency,
)
from fastapi import Request
# Add middleware to create scoped container per request
app.add_middleware(ScopedContainerMiddleware, container=container)
# Register scoped dependencies
container.register_scoped({
RequestContext: lambda c: RequestContext(),
RequestLogger: lambda c: RequestLogger(c.resolve(RequestContext)),
})
# Use scoped dependency in route
@app.get("/items")
async def list_items(
request: Request,
ctx: RequestContext = Depends(create_scoped_dependency(RequestContext))
):
# RequestContext is scoped to this request
# Same instance shared across all dependencies in this request
return {"request_id": ctx.request_id}
Complete FastAPI Example
from fastapi import FastAPI, Depends, Request
from miraveja_di import DIContainer
from miraveja_di.infrastructure.fastapi_integration import (
ScopedContainerMiddleware,
create_fastapi_dependency,
create_scoped_dependency,
)
# Initialize container
container = DIContainer()
# Register singletons (shared across all requests)
container.register_singletons({
AppConfig: lambda c: AppConfig.from_env(),
DatabaseConnection: lambda c: DatabaseConnection(c.resolve(AppConfig)),
CacheService: lambda c: CacheService(),
})
# Register scoped (per-request)
container.register_scoped({
RequestContext: lambda c: RequestContext(),
UnitOfWork: lambda c: UnitOfWork(c.resolve(DatabaseConnection)),
})
# Register transients (new instance each time)
container.register_transients({
UserService: lambda c: UserService(
c.resolve(UserRepository),
c.resolve(CacheService),
),
})
# Setup FastAPI
app = FastAPI()
app.add_middleware(ScopedContainerMiddleware, container=container)
# Create dependency injectors
get_user_service = create_fastapi_dependency(container, UserService)
get_request_context = create_scoped_dependency(RequestContext)
@app.get("/users/{user_id}")
async def get_user(
user_id: int,
user_service: UserService = Depends(get_user_service),
ctx: RequestContext = Depends(get_request_context),
):
ctx.log(f"Getting user {user_id}")
return await user_service.get_user(user_id)
🧪 Testing
Using TestContainer
The TestContainer allows you to create isolated test environments with mocked dependencies:
from miraveja_di.infrastructure.testing import TestContainer
import pytest
def test_user_service():
# Create test container (inherits from your main container)
test_container = TestContainer(container)
# Mock dependencies
mock_repo = MockUserRepository()
test_container.mock_singleton(UserRepository, mock_repo)
# Resolve service with mocked dependency
service = test_container.resolve(UserService)
# Test service
result = service.get_user(123)
assert result == expected_user
assert mock_repo.get_user.called_with(123)
Override Registrations
def test_with_override():
test_container = TestContainer(container)
# Override specific registration
test_container.register_singletons({
EmailService: lambda c: MockEmailService(),
})
service = test_container.resolve(NotificationService)
# EmailService is mocked, other dependencies are real
Testing Scoped Dependencies with MockScope
Use MockScope for testing request-scoped dependencies:
from miraveja_di.infrastructure.testing import MockScope
def test_request_handler():
# Create scoped context for testing
with MockScope(container) as scoped:
# Register request-specific mocks
scoped.register_scoped({
RequestContext: lambda c: RequestContext(user_id="test-user"),
})
# Resolve dependencies
handler = scoped.resolve(RequestHandler)
ctx = scoped.resolve(RequestContext)
# Same instance within scope
assert handler.context is ctx
assert ctx.user_id == "test-user"
# Scoped instances automatically cleaned up
Testing with Pytest Fixtures
import pytest
from miraveja_di import DIContainer
from miraveja_di.infrastructure.testing import TestContainer, create_mock_container
@pytest.fixture
def app_container():
"""Base container with real registrations."""
container = DIContainer()
container.register_singletons({
DatabaseConnection: lambda c: DatabaseConnection(),
CacheService: lambda c: CacheService(),
})
return container
@pytest.fixture
def test_container(app_container):
"""Test container with mocked external dependencies."""
test_container = TestContainer(app_container)
test_container.mock_singleton(DatabaseConnection, MockDatabase())
return test_container
def test_user_service_get_user(test_container):
service = test_container.resolve(UserService)
user = service.get_user(123)
assert user.id == 123
def test_user_service_create_user(test_container):
service = test_container.resolve(UserService)
new_user = service.create_user("John Doe")
assert new_user.name == "John Doe"
Quick Mock Container
For simple tests, use create_mock_container():
from miraveja_di.infrastructure.testing import create_mock_container
def test_simple_service():
# Create container with mocked singletons
mock_db = MockDatabase()
mock_cache = MockCache()
test_container = create_mock_container(
(DatabaseConnection, mock_db),
(CacheService, mock_cache),
)
service = test_container.resolve(UserService)
# UserService will use mocked db and cache
result = service.get_user(123)
assert result is not None
🔄 Migration from Other DI Frameworks
From dependency-injector
dependency-injector:
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
database = providers.Singleton(DatabaseConnection, config.db_url)
user_service = providers.Factory(UserService, database)
miraveja-di:
from miraveja_di import DIContainer
container = DIContainer()
container.register_singletons({
AppConfig: lambda c: AppConfig.from_env(),
DatabaseConnection: lambda c: DatabaseConnection(c.resolve(AppConfig).db_url),
})
container.register_transients({
UserService: lambda c: UserService(c.resolve(DatabaseConnection)),
})
From injector
injector:
from injector import Injector, inject, singleton
class DatabaseConnection:
pass
class UserService:
@inject
def __init__(self, db: DatabaseConnection):
self.db = db
injector = Injector()
injector.binder.bind(DatabaseConnection, to=DatabaseConnection, scope=singleton)
user_service = injector.get(UserService)
miraveja-di:
from miraveja_di import DIContainer
class DatabaseConnection:
pass
class UserService:
def __init__(self, db: DatabaseConnection):
self.db = db
container = DIContainer()
container.register_singletons({
DatabaseConnection: lambda c: DatabaseConnection(),
})
# UserService auto-wired via type hints
user_service = container.resolve(UserService)
From FastAPI Depends()
FastAPI Depends:
from fastapi import Depends
def get_database():
db = DatabaseConnection()
try:
yield db
finally:
db.close()
def get_user_service(db: DatabaseConnection = Depends(get_database)):
return UserService(db)
@app.get("/users/{user_id}")
async def get_user(service: UserService = Depends(get_user_service)):
return service.get_user(user_id)
miraveja-di with FastAPI:
from miraveja_di import DIContainer
from miraveja_di.infrastructure.fastapi_integration import (
ScopedContainerMiddleware,
create_fastapi_dependency,
)
container = DIContainer()
container.register_singletons({
DatabaseConnection: lambda c: DatabaseConnection(),
})
# UserService auto-wired
app.add_middleware(ScopedContainerMiddleware, container=container)
get_user_service = create_fastapi_dependency(container, UserService)
@app.get("/users/{user_id}")
async def get_user(service: UserService = Depends(get_user_service)):
return service.get_user(user_id)
Key Differences
| Feature | miraveja-di | dependency-injector | injector |
|---|---|---|---|
| Configuration Style | Imperative (dictionaries) | Declarative (containers) | Mixed |
| Auto-wiring | ✅ Type hints | ❌ Manual providers | ✅ Type hints |
| Scoped Lifetime | ✅ Built-in | ✅ Request scopes | ✅ Request scopes |
| FastAPI Integration | ✅ First-class | ✅ Via providers | ⚠️ Third-party |
| Testing Utilities | ✅ TestContainer, MockScope | ✅ Override providers | ⚠️ Manual |
| Learning Curve | Low (simple API) | Medium (declarative) | Medium (decorators) |
🔧 Advanced Usage
Factory Functions
def create_model_provider(model_type: str):
if model_type == "clip":
return CLIPModelProvider()
elif model_type == "dinov2":
return DINOv2ModelProvider()
return SAMModelProvider()
container.register_factories({
IModelProvider: lambda c: create_model_provider(
c.resolve(AppConfig).model_type
),
})
Circular Dependency Detection
# This will raise CircularDependencyError with full chain
class ServiceA:
def __init__(self, b: 'ServiceB'):
self.b = b
class ServiceB:
def __init__(self, a: ServiceA):
self.a = a
# CircularDependencyError: ServiceA -> ServiceB -> ServiceA
container.resolve(ServiceA)
Conditional Registration
config = AppConfig.from_env()
if config.use_cache:
container.register_singletons({
CacheService: lambda c: RedisCacheService(config.redis_url),
})
else:
container.register_singletons({
CacheService: lambda c: InMemoryCacheService(),
})
📚 API Reference
DIContainer
Registration Methods:
register_singletons(dependencies: dict[type, Callable[[DIContainer], Any]])- Register multiple singleton dependencies (one instance per application)register_transients(dependencies: dict[type, Callable[[DIContainer], Any]])- Register multiple transient dependencies (new instance per resolution)register_scoped(dependencies: dict[type, Callable[[DIContainer], Any]])- Register multiple scoped dependencies (one instance per scope)
Resolution Methods:
resolve(cls: type[T]) -> T- Resolve and return an instance of the specified type with auto-wiring
Scope Management:
create_scope() -> DIContainer- Create a child container for scoped lifetime (inherits parent registrations and singleton cache)__enter__() -> DIContainer- Enter context manager for scoped lifetime__exit__(exc_type, exc_val, exc_tb) -> None- Exit context manager and cleanup scoped instances
Utilities:
clear()- Clear all registrations and cached instancesget_registry_copy() -> dict[type, DependencyMetadata]- Get a copy of the current registryset_registry(registry: dict[type, DependencyMetadata])- Set the registry (used internally for scope creation)
Lifetime Enum
Lifetime.SINGLETON- Single instance for entire applicationLifetime.TRANSIENT- New instance per resolutionLifetime.SCOPED- Single instance per scope
Exceptions
DIException- Base exception for all DI errorsCircularDependencyError- Raised when circular dependencies are detectedUnresolvableError- Raised when a dependency cannot be resolvedLifetimeError- Raised for invalid lifetime configurationsScopeError- Raised when scoped instance requested outside scope
💡 Best Practices
- Register interfaces, not implementations: Register abstract interfaces and let the container provide concrete implementations
- Use constructor injection: Prefer constructor parameters over property injection
- Keep registrations centralized: Create a single
Dependencies.pymodule for registration - Leverage auto-wiring: Only register dependencies that can't be auto-wired
- Test with mocks: Use
TestContainerto override dependencies in tests - Scope per request: Use scoped lifetime for request-specific state in web applications
🚧 Development Status
Active Development - Core features implemented and tested
✅ Completed:
- Dependency injection container with auto-wiring
- Singleton, Transient, and Scoped lifetimes
- Circular dependency detection
- FastAPI integration with middleware support
- Testing utilities (TestContainer, MockScope)
- Exception handling and error reporting
- 96%+ test coverage
🚀 Planned Features:
- Async initialization support
- Decorator-based registration
- Configuration validation
- Performance optimizations
See the architecture plan for detailed implementation notes.
🤝 Contributinging
Contributions are welcome! Please follow the existing code structure and ensure all tests pass.
# Run all tests
poetry run pytest
# Generate coverage report
poetry run pytest --cov=miraveja_di --cov-report=html
# Run linting
poetry run pylint src/miraveja_di
# Format code
poetry run black .
# Sort imports
poetry run isort .
# Run pre-commit hooks
poetry run pre-commit run --all-files
📄 License
MIT License - See LICENSE file for details.
👨💻 Author
Jomar Júnior de Souza Pereira - jomarjunior@poli.ufrj.br
Part of the Miraveja ecosystem - A modern image gallery and management platform.
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 miraveja_di-0.1.0.tar.gz.
File metadata
- Download URL: miraveja_di-0.1.0.tar.gz
- Upload date:
- Size: 23.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b6a437f49ad11ce18830933040b142283fb91d0de8818a0ebd627b221f9f6b06
|
|
| MD5 |
c43d9816e202410840f507ab4a6d6529
|
|
| BLAKE2b-256 |
fa4c9910a113289629e125844806e7ed37f8d9821b54ee116602051b4a58fc58
|
Provenance
The following attestation bundles were made for miraveja_di-0.1.0.tar.gz:
Publisher:
publish.yml on JomarJunior/miraveja-di
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
miraveja_di-0.1.0.tar.gz -
Subject digest:
b6a437f49ad11ce18830933040b142283fb91d0de8818a0ebd627b221f9f6b06 - Sigstore transparency entry: 711525538
- Sigstore integration time:
-
Permalink:
JomarJunior/miraveja-di@33ea374d895230889ebb3e62ea50d164128d2903 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/JomarJunior
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@33ea374d895230889ebb3e62ea50d164128d2903 -
Trigger Event:
release
-
Statement type:
File details
Details for the file miraveja_di-0.1.0-py3-none-any.whl.
File metadata
- Download URL: miraveja_di-0.1.0-py3-none-any.whl
- Upload date:
- Size: 24.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dae92d8e1ea1cbf55280addd7fac074f6f9c4eb7cdda1f6e46cf79cc99fdd500
|
|
| MD5 |
bbaf1f8a26cfdc88f9260f496e9e0690
|
|
| BLAKE2b-256 |
f33635349bcd4fc8ab7adfc109573914e4db5b49b5b00359889290a892228ff3
|
Provenance
The following attestation bundles were made for miraveja_di-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on JomarJunior/miraveja-di
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
miraveja_di-0.1.0-py3-none-any.whl -
Subject digest:
dae92d8e1ea1cbf55280addd7fac074f6f9c4eb7cdda1f6e46cf79cc99fdd500 - Sigstore transparency entry: 711525541
- Sigstore integration time:
-
Permalink:
JomarJunior/miraveja-di@33ea374d895230889ebb3e62ea50d164128d2903 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/JomarJunior
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@33ea374d895230889ebb3e62ea50d164128d2903 -
Trigger Event:
release
-
Statement type: