Skip to main content

Yet another Python dependency injection framework

Project description

RetroSys Core

A powerful, flexible Python dependency injection framework with support for synchronous and asynchronous services.

Overview

RetroSys Core provides a modern dependency injection system for Python applications, designed to:

  • Simplify application architecture through dependency inversion
  • Support both synchronous and asynchronous dependency resolution
  • Provide lifecycle management (singleton, transient, and scoped instances)
  • Support property and method injection alongside constructor injection
  • Enable modular application design
  • Facilitate testing through mock capabilities

The framework is designed to be lightweight yet powerful, with a focus on type safety and developer experience.

Features

  • Multiple Injection Methods:

    • Constructor injection (using type hints)
    • Property injection (using decorators)
    • Method injection (using decorators)
  • Lifecycle Management:

    • Singleton: One instance per container
    • Transient: New instance each time it's requested
    • Scoped: One instance per scope (useful for web request contexts)
  • Async Support:

    • Asynchronous dependency resolution
    • Async factory functions
    • Async initialization and cleanup
  • Advanced Features:

    • Contextual binding (register different implementations with context keys)
    • Automatic registration with @injectable decorator
    • Circular dependency detection
    • Lazy dependency resolution
    • Module system for organizing registrations
    • Mock capabilities for testing

Installation

pip install retrosys-core

Getting Started

Basic Usage

Here's a simple example that demonstrates the basics of dependency injection:

from retrosys.core.dependency_injection import Container, injectable

# Define service classes with @injectable decorator
@injectable()
class DatabaseService:
    def get_data(self):
        return ["item1", "item2", "item3"]

@injectable()
class UserService:
    def __init__(self, database_service: DatabaseService):
        self.database_service = database_service
        
    def get_user_data(self):
        return self.database_service.get_data()

# Create container and resolve services
container = Container()
user_service = container.resolve(UserService)
data = user_service.get_user_data()
print(data)  # Output: ["item1", "item2", "item3"]

Lifecycle Management

Control how your services are instantiated:

from retrosys.core.dependency_injection import Container, injectable, Lifecycle

@injectable(lifecycle=Lifecycle.SINGLETON)
class ConfigService:
    def __init__(self):
        print("ConfigService initialized")
        self.settings = {"api_url": "https://api.example.com"}

@injectable(lifecycle=Lifecycle.TRANSIENT)
class RequestHandler:
    def __init__(self, config: ConfigService):
        print("RequestHandler initialized")
        self.config = config

# Both handlers share the same ConfigService instance
container = Container()
handler1 = container.resolve(RequestHandler)
handler2 = container.resolve(RequestHandler)

# Output:
# ConfigService initialized
# RequestHandler initialized 
# RequestHandler initialized

Property Injection

Use property injection when constructor injection isn't suitable:

from retrosys.core.dependency_injection import Container, injectable, inject_property

@injectable()
class LogService:
    def log(self, message):
        print(f"LOG: {message}")

@injectable()
class UserController:
    # Property injection with getter/setter
    @inject_property(LogService)
    def logger(self):
        pass
    
    def create_user(self, username):
        # Logger will be automatically resolved when accessed
        self.logger.log(f"Creating user: {username}")
        return {"id": 1, "username": username}

container = Container()
controller = container.resolve(UserController)
controller.create_user("john")  # Output: LOG: Creating user: john

Async Support

Use async for service initialization and resolution:

import asyncio
from retrosys.core.dependency_injection import Container, injectable

@injectable(is_async=True)
class AsyncDatabaseService:
    async def __init__(self):
        # Simulate async initialization
        await asyncio.sleep(0.1)
        self.connection = "db_connection"
        print("Database connected")
        
    async def get_data(self):
        await asyncio.sleep(0.1)  # Simulate database query
        return ["async_item1", "async_item2"]

@injectable(is_async=True)
class AsyncUserService:
    def __init__(self, db: AsyncDatabaseService):
        self.db = db
        
    async def get_users(self):
        return await self.db.get_data()

async def main():
    container = Container()
    # Use resolve_async for async services
    user_service = await container.resolve_async(AsyncUserService)
    users = await user_service.get_users()
    print(users)

asyncio.run(main())

Modules

Organize your registrations using modules:

from retrosys.core.dependency_injection import Container, injectable, register_module

@injectable()
class Service1:
    pass

@injectable()
class Service2:
    pass

# Create a module class to group related services
container = Container()

@register_module(container)
class InfrastructureModule:
    # All injectable classes defined in this module will be registered
    @injectable()
    class DatabaseService:
        def get_connection(self):
            return "database_connection"
    
    @injectable()
    class CacheService:
        def cache(self, key, value):
            print(f"Caching {key}: {value}")

# Now you can resolve services defined in the module
db_service = container.resolve(InfrastructureModule.DatabaseService)
print(db_service.get_connection())  # Output: database_connection

Testing with Mocks

Easily mock dependencies for testing:

from retrosys.core.dependency_injection import Container, injectable
import unittest

@injectable()
class EmailService:
    def send_email(self, to, subject, body):
        # In production, this would send an actual email
        return True

@injectable()
class UserService:
    def __init__(self, email_service: EmailService):
        self.email_service = email_service
    
    def register_user(self, email):
        # Business logic...
        self.email_service.send_email(
            email, 
            "Welcome!", 
            "Thank you for registering."
        )
        return True

class TestUserService(unittest.TestCase):
    def test_register_user(self):
        # Create container in test mode
        container = Container().enable_test_mode()
        
        # Create a mock email service
        class MockEmailService:
            def __init__(self):
                self.emails_sent = []
                
            def send_email(self, to, subject, body):
                self.emails_sent.append((to, subject, body))
                return True
        
        # Register the mock
        mock_email = MockEmailService()
        container.mock(EmailService, mock_email)
        
        # Resolve the service under test with the mock
        user_service = container.resolve(UserService)
        
        # Execute the method being tested
        result = user_service.register_user("user@example.com")
        
        # Assertions
        self.assertTrue(result)
        self.assertEqual(len(mock_email.emails_sent), 1)
        self.assertEqual(mock_email.emails_sent[0][0], "user@example.com")
        
        # Clean up
        container.disable_test_mode()

Advanced Usage

Factory Registration

Use factory functions for complex initialization:

from retrosys.core.dependency_injection import Container, Lifecycle

# Container instance
container = Container()

# Define a factory function
def create_database_connection(container):
    # Complex initialization logic
    connection_string = "db://example"
    max_connections = 10
    return {"connection": connection_string, "pool_size": max_connections}

# Register the factory
container.register_factory(
    dict,  # Service type
    create_database_connection,  # Factory function
    lifecycle=Lifecycle.SINGLETON,  # Lifecycle
    context_key="db_config"  # Optional context key
)

# Resolve with context key
db_config = container.resolve(dict, context_key="db_config")
print(db_config)  # Output: {'connection': 'db://example', 'pool_size': 10}

Scoped Lifecycle

Manage dependencies for specific operations:

from retrosys.core.dependency_injection import Container, injectable, Lifecycle

@injectable(lifecycle=Lifecycle.SCOPED)
class RequestContext:
    def __init__(self):
        self.user_id = None
        self.request_id = None

@injectable()
class UserRepository:
    def __init__(self, context: RequestContext):
        self.context = context
    
    def get_user_data(self):
        return f"Data for user {self.context.user_id}"

# Create container and scope
container = Container()
scope = container.create_scope()

# Configure scope-specific data
request_context = scope.resolve(RequestContext)
request_context.user_id = "user123"
request_context.request_id = "req456"

# Resolve service within scope
repo = scope.resolve(UserRepository)
print(repo.get_user_data())  # Output: Data for user user123

# Different scope gets different instance
scope2 = container.create_scope()
request_context2 = scope2.resolve(RequestContext)
request_context2.user_id = "user999"
repo2 = scope2.resolve(UserRepository)
print(repo2.get_user_data())  # Output: Data for user user999

API Reference Highlights

Main Components

  • Container: The central dependency container
  • Lifecycle: Enum defining service lifecycles (SINGLETON, TRANSIENT, SCOPED)
  • @injectable: Decorator for auto-registration of services
  • @inject_property: Decorator for property injection
  • @inject_method: Decorator for method injection

Container Methods

  • register: Register a service with the container
  • register_instance: Register an existing instance
  • register_factory: Register a factory function
  • resolve: Resolve a service instance
  • resolve_async: Asynchronously resolve a service
  • create_scope: Create a dependency scope
  • register_module: Register a module with the container
  • enable_test_mode / disable_test_mode: Methods for testing
  • mock: Register a mock instance for testing

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the 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

retrosys_core-0.1.0.tar.gz (37.6 kB view details)

Uploaded Source

Built Distribution

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

retrosys_core-0.1.0-py3-none-any.whl (29.8 kB view details)

Uploaded Python 3

File details

Details for the file retrosys_core-0.1.0.tar.gz.

File metadata

  • Download URL: retrosys_core-0.1.0.tar.gz
  • Upload date:
  • Size: 37.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for retrosys_core-0.1.0.tar.gz
Algorithm Hash digest
SHA256 6d081a690577f8f012e9a4e29f6c4a73ffa2cf5a9a0514d5b63cc87c1b71e957
MD5 5d97c4233369e536810be691d0da515d
BLAKE2b-256 16a1306310f99aaf1cf3d4c25b5ceb70caa213400ba970e8ab3868610762d496

See more details on using hashes here.

File details

Details for the file retrosys_core-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: retrosys_core-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 29.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for retrosys_core-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 582da10b757fa5dd16681c92312a2917bff4acf5fe9ea9aadb49414340057e0e
MD5 8bacd6b9e1de19a6b5f9cee168a34b72
BLAKE2b-256 1d9051fa1ba3aede1c7844deb21f82e9bf0f003734bfbc707a2cfa077f50a859

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