A simple, lightweight dependency injection library for Python based on constructor parameter name matching.
Project description
abcDI
A simple, lightweight dependency injection library for Python based on constructor parameter name matching.
Features
- Zero external dependencies - Uses only Python standard library
- Parameter-based injection - Automatically injects dependencies based on parameter names
- Explicit injection sentinels - Use
injected()for explicit, non-magical dependency injection - Multiple contexts - Support for isolated dependency scopes
- Lazy and eager loading - Create dependencies when needed or upfront
- Global context management - Set and retrieve contexts globally for easier usage
Installation
pip install abcdi
Quick Start
import abcdi
# Define your classes
class Database:
def __init__(self, connection_string: str):
self.connection_string = connection_string
class UserService:
def __init__(self, database: Database): # Note: parameter name matches dependency name
self.database = database
# Create dependencies configuration using new factory() and instance() functions
dependencies = {
'database': abcdi.factory(Database, connection_string='sqlite:///app.db'),
'user_service': abcdi.factory(UserService), # Will auto-inject 'database'
}
# Set global context
abcdi.set_context(dependencies)
# Get dependencies
user_service = abcdi.get_dependency('user_service')
print(user_service.database.connection_string) # sqlite:///app.db
Core Concepts
Dependencies Configuration
Dependencies are defined as a dictionary using factory() and instance() helper functions:
import abcdi
dependencies = {
'my_service': abcdi.factory(MyService, arg1, arg2, keyword='value'),
'config': abcdi.instance(existing_config_object),
'database': abcdi.factory(Database, url='sqlite:///app.db')
}
- **
factory(Class, \*args, **kwargs)**: Creates instances of the class with dependency injection instance(obj): Uses an existing object as a dependency
Automatic Injection
Dependencies are automatically injected based on constructor parameter names:
class ServiceA:
def __init__(self, database: Database): # 'database' matches dependency name
self.database = database
class ServiceB:
def __init__(self, service_a: ServiceA, database: Database):
self.service_a = service_a # Gets the 'service_a' dependency
self.database = database # Gets the 'database' dependency
Explicit Injection with Sentinels
For more explicit control, use injection sentinels with default parameters:
import abcdi
# Using global context with default parameter injection
@abcdi.injectable
def process_users(data: str, user_service=abcdi.injected('user_service'), db=abcdi.injected('database')):
return user_service.process_data(data, db)
# Using specific context
@abcdi.injectable
def process_orders(order_service=abcdi.injected()):
return order_service.get_all_orders()
# Call without providing dependencies - they're auto-injected from default values
users = process_users("user_data") # user_service and db injected automatically
orders = process_orders() # order_service injected from parameter name
# Can still override specific dependencies
users = process_users("user_data", user_service=custom_service)
Usage Patterns
1. Global Context
Set a global context once and use convenience functions:
import abcdi
# Setup
abcdi.set_context(dependencies)
# Usage anywhere in your code
db = abcdi.get_dependency('database')
result = abcdi.call(some_function) # Auto-injects dependencies
2. Direct Context Usage
Use contexts directly for more control:
ctx = abcdi.Context(dependencies)
db = ctx.get_dependency('database')
result = ctx.call(some_function)
# Context manager support
with abcdi.Context(dependencies) as ctx:
service = ctx.get_dependency('my_service')
# Context persists after exiting the with block
3. Sub-contexts
Create child contexts that inherit and override dependencies:
# Parent context
parent_deps = {
'database': abcdi.factory(Database, url='sqlite:///app.db'),
'logger': abcdi.factory(Logger, level='INFO')
}
abcdi.set_context(parent_deps)
# Child context with temporary overrides
child_deps = {
'user_service': abcdi.factory(UserService), # Inherits database from parent
'logger': abcdi.factory(Logger, level='DEBUG') # Override parent's logger
}
# Method 1: Direct subcontext
with abcdi.Context(parent_deps).subcontext(child_deps) as child_ctx:
service = child_ctx.get_dependency('user_service')
# Method 2: Global subcontext (temporarily changes global context)
with abcdi.subcontext(child_deps) as child_ctx:
service = abcdi.get_dependency('user_service') # Uses global context
# Original global context is restored here
# Method 3: Hidden dependencies (prevent inheritance of specific dependencies)
hidden_deps = {'logger'} # Don't inherit logger from parent
with abcdi.subcontext(child_deps, hidden_dependencies=hidden_deps) as child_ctx:
# This context won't see parent's logger, only its own
service = child_ctx.get_dependency('user_service')
4. Function Decoration
Bind dependencies to functions:
@abcdi.bind_dependencies
def process_users(user_service: UserService):
return user_service.get_all_users()
# Call without arguments - dependencies auto-injected
users = process_users()
Advanced Features
Lazy vs Eager Loading
# Eager loading (default) - creates all dependencies immediately
ctx = abcdi.Context(dependencies, lazy=False)
# Lazy loading - creates dependencies only when requested for the first time.
ctx = abcdi.Context(dependencies, lazy=True)
Hidden Dependencies
Prevent child contexts from inheriting specific dependencies from parent contexts:
parent_deps = {
'database': abcdi.factory(Database, url='prod://db'),
'logger': abcdi.factory(Logger, level='ERROR')
}
abcdi.set_context(parent_deps)
# Child context that blocks inheritance of certain dependencies
child_deps = {
'database': abcdi.factory(Database, url='test://db'), # Override parent
}
# Hide 'logger' - child won't inherit it from parent
hidden_deps = {'logger'}
with abcdi.subcontext(child_deps, hidden_dependencies=hidden_deps) as ctx:
db = ctx.get_dependency('database') # Gets test database
# ctx.get_dependency('logger') # Would raise KeyError - hidden from parent
Explicit Parameter Override
You can override auto-injection with explicit parameters:
# This will use the provided database instead of the injected one
result = abcdi.call(some_function, database=my_custom_db)
Default Parameter Injection
The @injectable decorator automatically processes default parameters with injection sentinels:
@abcdi.injectable
def send_email(
message: str,
email_service=abcdi.injected('email_service'),
logger=abcdi.injected() # Uses parameter name 'logger'
):
logger.info(f"Sending email: {message}")
return email_service.send(message)
# Call without providing dependencies
send_email("Hello World") # email_service and logger auto-injected from defaults
Circular Dependency Detection
The library automatically detects and prevents circular dependencies:
# This will raise ValueError: "Circular dependency detected"
dependencies = {
'service_a': (ServiceA, [], {}), # ServiceA needs service_b
'service_b': (ServiceB, [], {}), # ServiceB needs service_a
}
API Reference
Global Functions
abcdi.set_context(dependencies, lazy=False)- Set the global DI context with dependencies dictabcdi.context()- Get the current global DI contextabcdi.get_dependency(name)- Get a dependency from global contextabcdi.call(callable_obj, *args, **kwargs)- Call function with dependency injectionabcdi.bind_dependencies(callable_obj)- Return function with dependencies boundabcdi.subcontext(dependencies, lazy=False, hidden_dependencies=None)- Create temporary global subcontext (context manager)abcdi.injected(name)- Create injection sentinel for explicit dependency injectionabcdi.injectable(callable_obj)- Decorator that processes injection sentinels in function callsabcdi.factory(Class, *args, **kwargs)- Create factory configuration for dependency injectionabcdi.instance(obj)- Create instance configuration for existing objects
Context Class
class Context:
def __init__(self, dependencies: dict[str, dict[str, Any]], lazy: bool = False, parent: Context | None = None, hidden_dependencies: set[str] | None = None)
def get_dependency(self, name: str) -> Any
def call(self, callable_obj, *args, **kwargs) -> Any
def bind_dependencies(self, callable_obj) -> Callable
def has_dependency(self, name: str) -> bool
def subcontext(self, dependencies: dict[str, dict[str, Any]], lazy: bool = False, hidden_dependencies: set[str] | None = None) -> Context
def injected(self, dependency_name: str | None = None) -> InjectedSentinel
def __enter__(self) -> Context # Context manager support
def __exit__(self, exc_type, exc_val, exc_tb) -> None
Examples
Web Application Setup
import abcdi
from myapp.database import Database
from myapp.services import UserService, OrderService
from myapp.repositories import UserRepository, OrderRepository
dependencies = {
'database': abcdi.factory(Database, url='postgresql://localhost/myapp'),
'user_repository': abcdi.factory(UserRepository),
'order_repository': abcdi.factory(OrderRepository),
'user_service': abcdi.factory(UserService),
'order_service': abcdi.factory(OrderService),
}
abcdi.set_context(dependencies)
# Now your controllers can use dependency injection
def get_user_orders(user_id: int, order_service: OrderService):
return order_service.get_orders_for_user(user_id)
# Call with auto-injection
orders = abcdi.call(get_user_orders, user_id=123)
Testing with Mocks
import unittest
from unittest.mock import Mock
import abcdi
class TestUserService(unittest.TestCase):
def setUp(self):
# Create test dependencies with mocks
mock_db = Mock()
test_dependencies = {
'database': abcdi.instance(mock_db),
'user_service': abcdi.factory(UserService),
}
abcdi.set_context(test_dependencies)
def test_user_creation(self):
user_service = abcdi.get_dependency('user_service')
# Test your service...
Explicit Injection Example
import abcdi
# Setup dependencies
dependencies = {
'database': abcdi.factory(Database, connection_string='sqlite:///app.db'),
'user_service': abcdi.factory(UserService),
'email_service': abcdi.factory(EmailService),
}
abcdi.set_context(dependencies)
# Function using explicit injection sentinels
@abcdi.injectable
def send_welcome_email(
user_id: int,
user_svc=abcdi.injected('user_service'), # Explicit dependency name
email_svc=abcdi.injected('email_service') # Different param name than dependency
):
user = user_svc.get_user(user_id)
return email_svc.send_welcome(user.email)
# Call without providing dependencies - they're auto-injected
result = send_welcome_email(user_id=123)
# Can still override specific dependencies
custom_email_service = CustomEmailService()
result = send_welcome_email(user_id=123, email_svc=custom_email_service)
Error Handling
The library provides clear error messages for common issues:
KeyError- When requesting a dependency that doesn't existValueError- When circular dependencies are detectedRuntimeError- When no global context is set
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT License - see LICENSE file for details.
Project details
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 abcdi-0.7.0.tar.gz.
File metadata
- Download URL: abcdi-0.7.0.tar.gz
- Upload date:
- Size: 16.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
758a6fe55351777193d5f602037a1a7d6e3184403c099a64301e390fe4bb4f10
|
|
| MD5 |
9855fc5d3acf5c7121d5218f33976a08
|
|
| BLAKE2b-256 |
a919b998a71c6c314dc572301c9f567a5c0fc1d7915258822fe25cf0a5393b76
|
File details
Details for the file abcdi-0.7.0-py3-none-any.whl.
File metadata
- Download URL: abcdi-0.7.0-py3-none-any.whl
- Upload date:
- Size: 9.6 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 |
f47deb9b148900ce2f9bbd4e03664ae65a3e8b6ba2066c987bdda45434c0a237
|
|
| MD5 |
8c8c813be9f8ecdad5da20e22410992d
|
|
| BLAKE2b-256 |
0a5520e328b3c16fbe1f7109eebe8dd41c2ff2549d07eab77030f89ddaff0126
|