Skip to main content

💉 miraveja-di

Python Version License Code Style Status Coverage CI

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 instances
  • get_registry_copy() -> dict[type, DependencyMetadata] - Get a copy of the current registry
  • set_registry(registry: dict[type, DependencyMetadata]) - Set the registry (used internally for scope creation)

Lifetime Enum

  • Lifetime.SINGLETON - Single instance for entire application
  • Lifetime.TRANSIENT - New instance per resolution
  • Lifetime.SCOPED - Single instance per scope

Exceptions

  • DIException - Base exception for all DI errors
  • CircularDependencyError - Raised when circular dependencies are detected
  • UnresolvableError - Raised when a dependency cannot be resolved
  • LifetimeError - Raised for invalid lifetime configurations
  • ScopeError - Raised when scoped instance requested outside scope

💡 Best Practices

  1. Register interfaces, not implementations: Register abstract interfaces and let the container provide concrete implementations
  2. Use constructor injection: Prefer constructor parameters over property injection
  3. Keep registrations centralized: Create a single Dependencies.py module for registration
  4. Leverage auto-wiring: Only register dependencies that can't be auto-wired
  5. Test with mocks: Use TestContainer to override dependencies in tests
  6. 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.

Metadata

Release files for miraveja-di 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for miraveja-di 0.2.0
File Size Uploaded
miraveja_di-0.2.0.tar.gz 23.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for miraveja-di 0.2.0
File Interpreter ABI Platform
miraveja_di-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 47.8 kB

Release files / miraveja_di-0.2.0.tar.gz

Download URL miraveja_di-0.2.0.tar.gz
Size 23.2 kB
Tags Source
SHA-256 checksum
How to use checksums
6c39bf45c1816987c6066f5e6cc14ffc12b66cc1d473da31b8cd833b3bcf161d
BLAKE2b-256 checksum
How to use checksums
eaa0eb5ddae52e0de8aa28b912c8f0219c4d830b46e32c4710acf5bb5d133265
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 28, 2026.

Transparency log

Release files / miraveja_di-0.2.0-py3-none-any.whl

Download URL miraveja_di-0.2.0-py3-none-any.whl
Size 24.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
aa0b845520edc0d8b3768fb10d56768f3bf93fbd577465c67189266f066e5f6e
BLAKE2b-256 checksum
How to use checksums
04bdc15d4ee9ad0c302d49248f18fb7e9cf6c20ced53bb62d9aceae1d2688713
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 28, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page