Skip to main content

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 dict
  • abcdi.context() - Get the current global DI context
  • abcdi.get_dependency(name) - Get a dependency from global context
  • abcdi.call(callable_obj, *args, **kwargs) - Call function with dependency injection
  • abcdi.bind_dependencies(callable_obj) - Return function with dependencies bound
  • abcdi.subcontext(dependencies, lazy=False, hidden_dependencies=None) - Create temporary global subcontext (context manager)
  • abcdi.injected(name) - Create injection sentinel for explicit dependency injection
  • abcdi.injectable(callable_obj) - Decorator that processes injection sentinels in function calls
  • abcdi.factory(Class, *args, **kwargs) - Create factory configuration for dependency injection
  • abcdi.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 exist
  • ValueError - When circular dependencies are detected
  • RuntimeError - 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

abcdi-0.7.0.tar.gz (16.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

abcdi-0.7.0-py3-none-any.whl (9.6 kB view details)

Uploaded Python 3

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

Hashes for abcdi-0.7.0.tar.gz
Algorithm Hash digest
SHA256 758a6fe55351777193d5f602037a1a7d6e3184403c099a64301e390fe4bb4f10
MD5 9855fc5d3acf5c7121d5218f33976a08
BLAKE2b-256 a919b998a71c6c314dc572301c9f567a5c0fc1d7915258822fe25cf0a5393b76

See more details on using hashes here.

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

Hashes for abcdi-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f47deb9b148900ce2f9bbd4e03664ae65a3e8b6ba2066c987bdda45434c0a237
MD5 8c8c813be9f8ecdad5da20e22410992d
BLAKE2b-256 0a5520e328b3c16fbe1f7109eebe8dd41c2ff2549d07eab77030f89ddaff0126

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page