Metadata-Version: 2.4
Name: chibi-izumi
Version: 0.1.0
Summary: Simplified implementation of Staged Dependency Injection from Scala's distage library of Izumi Project
Project-URL: Homepage, https://github.com/7mind/izumi-chibi-py
Project-URL: Issues, https://github.com/7mind/izumi-chibi-py/issues
Author-email: Pavel Shirshov <pavel@7mind.io>
License-Expression: MIT
License-File: LICENSE
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.12.0
Requires-Dist: beartype>=0.19.0
Description-Content-Type: text/markdown

# Chibi Izumi

A Python re-implementation of some core concepts from Scala's [Izumi Project](https://github.com/7mind/izumi),
`distage` staged dependency injection library in particular.

The port was done by guiding Claude with thorough manual reviews.

At this point the project is not battle-tested. Expect dragons, landmines and varying mileage.

## Features

`distage` provides a powerful, type-safe dependency injection framework with:

- **Non-invasive design** - No decorators, base classes, or framework-specific code required in your business logic
- **Fluent DSL for defining bindings** - Type-safe API with `.using().value()/.type()/.func()/.factory()`
- **Signature introspection** - Automatic extraction of dependency requirements from type hints
- **Dependency graph formation and validation** - Build and validate the complete dependency graph at startup
- **Automatic logger injection** - Seamless injection of location-based loggers without manual setup
- **Factory bindings** - Create new instances on-demand with assisted injection (`Factory[T]`)
- **Named dependencies** - Distinguished dependencies using `@Id` annotations
- **Roots for dependency tracing** - Specify what components should be instantiated
- **Activations for configuration** - Choose between alternative implementations using configuration axes
- **Garbage collection** - Only instantiate components reachable from roots
- **Circular dependency detection** - Early detection of circular dependencies
- **Missing dependency detection** - Ensure all required dependencies are available
- **Tagged bindings** - Support for multiple implementations of the same interface
- **Set bindings** - Collect multiple implementations into sets

## Quick Start

```python
from izumi.distage import ModuleDef, Injector, PlannerInput

# Define your classes
class Database:
    def __init__(self, connection_string: str):
        self.connection_string = connection_string

    def query(self, sql: str) -> str:
        return f"DB[{self.connection_string}]: {sql}"

class UserService:
    def __init__(self, database: Database):
        self.database = database

    def create_user(self, name: str) -> str:
        return self.database.query(f"INSERT INTO users (name) VALUES ('{name}')")

# Configure bindings using the new fluent API
module = ModuleDef()
module.make(str).using().value("postgresql://prod:5432/app")
module.make(Database).using().type(Database)  # Constructor injection
module.make(UserService).using().type(UserService)

# Create injector and get service
injector = Injector()
planner_input = PlannerInput([module])
user_service = injector.get(planner_input, UserService)

# Use the service
result = user_service.create_user("alice")
print(result)  # DB[postgresql://prod:5432/app]: INSERT INTO users (name) VALUES ('alice')
```

## Core Concepts

### ModuleDef - Binding Definition DSL

The `ModuleDef` class provides a fluent DSL for defining dependency bindings using the new algebraic data structure approach:

```python
from izumi.distage import ModuleDef, Factory

# Example classes for demonstration
class Config:
    def __init__(self, debug: bool = False, db_url: str = ""):
        self.debug = debug
        self.db_url = db_url

class Database:
    def __init__(self, connection_string: str):
        self.connection_string = connection_string

class PostgresDatabase(Database):
    def __init__(self, connection_string: str):
        super().__init__(connection_string)

class UserService:
    def __init__(self, database: Database):
        self.database = database

class Handler:
    def handle(self):
        pass

class UserHandler(Handler):
    def handle(self):
        return "user"

class AdminHandler(Handler):
    def handle(self):
        return "admin"

# Now define the bindings
module = ModuleDef()

# Instance binding
module.make(Config).using().value(Config(debug=True))

# Class binding (constructor injection)
module.make(Database).using().type(PostgresDatabase)

# Factory function binding
def create_database(config: Config) -> Database:
    return Database(config.db_url)

module.make(Database).named("custom").using().func(create_database)

# Factory bindings for non-singleton semantics
module.make(Factory[UserService]).using().factory(UserService)

# Named bindings for multiple instances
module.make(str).named("db-url").using().value("postgresql://prod:5432/app")
module.make(str).named("api-key").using().value("secret-key-123")

# Set bindings for collecting multiple implementations
module.many(Handler).add_type(UserHandler)
module.many(Handler).add_type(AdminHandler)
```

### Automatic Logger Injection

Chibi Izumi automatically provides loggers for dependencies without names, creating location-specific logger instances:

```python
import logging
from izumi.distage import ModuleDef, Injector, PlannerInput

class Database:
    def __init__(self, connection_string: str):
        self.connection_string = connection_string

    def query(self, sql: str) -> str:
        return f"DB[{self.connection_string}]: {sql}"

class UserService:
    # Logger automatically injected based on class location
    def __init__(self, database: Database, logger: logging.Logger):
        self.database = database
        self.logger = logger  # Will be logging.getLogger("__main__.UserService")

    def create_user(self, name: str) -> str:
        self.logger.info(f"Creating user: {name}")
        return self.database.query(f"INSERT INTO users (name) VALUES ('{name}')")

# No need to configure loggers - they're injected automatically!
module = ModuleDef()
module.make(str).using().value("postgresql://prod:5432/app")
module.make(Database).using().type(Database)
module.make(UserService).using().type(UserService)

injector = Injector()
planner_input = PlannerInput([module])
user_service = injector.get(planner_input, UserService)
```

### Factory Bindings for Non-Singleton Semantics

Use `Factory[T]` when you need to create multiple instances with assisted injection:

```python
from typing import Annotated
from izumi.distage import Factory, Id, ModuleDef, Injector, PlannerInput

class Database:
    def __init__(self, connection_string: str):
        self.connection_string = connection_string

class UserSession:
    def __init__(self, database: Database, user_id: str, api_key: Annotated[str, Id("api-key")]):
        self.database = database
        self.user_id = user_id
        self.api_key = api_key

module = ModuleDef()
module.make(str).using().value("postgresql://prod:5432/app")
module.make(Database).using().type(Database)
module.make(Factory[UserSession]).using().factory(UserSession)

injector = Injector()
planner_input = PlannerInput([module])
factory = injector.get(planner_input, Factory[UserSession])

# Create new instances with runtime parameters
session1 = factory.create("user123", **{"api-key": "secret1"})
session2 = factory.create("user456", **{"api-key": "secret2"})
# Database is injected from DI, user_id and api_key are provided at creation time
```

### Named Dependencies with @Id

Use `@Id` annotations to distinguish between multiple bindings of the same type:

```python
from typing import Annotated
from izumi.distage import Id, ModuleDef, Injector, PlannerInput

class DatabaseService:
    def __init__(
        self,
        primary_db: Annotated[str, Id("primary-db")],
        replica_db: Annotated[str, Id("replica-db")]
    ):
        self.primary_db = primary_db
        self.replica_db = replica_db

module = ModuleDef()
module.make(str).named("primary-db").using().value("postgresql://primary:5432/app")
module.make(str).named("replica-db").using().value("postgresql://replica:5432/app")
module.make(DatabaseService).using().type(DatabaseService)

injector = Injector()
planner_input = PlannerInput([module])
db_service = injector.get(planner_input, DatabaseService)
```

### Dependency Graph Validation

The dependency graph is built and validated when creating a plan:

```python
from izumi.distage import ModuleDef, Injector, PlannerInput

class A:
    def __init__(self, b: "B"):
        self.b = b

class B:
    def __init__(self, a: A):
        self.a = a

# This will detect circular dependencies
module = ModuleDef()
module.make(A).using().type(A)
module.make(B).using().type(B)

try:
    injector = Injector()
    planner_input = PlannerInput([module])
    plan = injector.plan(planner_input)  # Validation happens here
    print("This should not print - circular dependency should be caught")
except Exception as e:
    # Catches circular dependencies, missing dependencies, etc.
    pass  # Expected to happen
```

### Set Bindings

Collect multiple implementations into a set:

```python
from izumi.distage import ModuleDef, Injector, PlannerInput

class CommandHandler:
    def handle(self, cmd: str) -> str:
        pass

class UserHandler(CommandHandler):
    def handle(self, cmd: str) -> str:
        return f"User: {cmd}"

class AdminHandler(CommandHandler):
    def handle(self, cmd: str) -> str:
        return f"Admin: {cmd}"

class CommandProcessor:
    def __init__(self, handlers: set[CommandHandler]):
        self.handlers = handlers

module = ModuleDef()
module.many(CommandHandler).add_type(UserHandler)
module.many(CommandHandler).add_type(AdminHandler)
module.make(CommandProcessor).using().type(CommandProcessor)

injector = Injector()
planner_input = PlannerInput([module])
processor = injector.get(planner_input, CommandProcessor)
# processor.handlers contains instances of both UserHandler and AdminHandler
```

## Advanced Usage Patterns

### Multiple Execution Patterns

```python
from izumi.distage import ModuleDef, Injector, PlannerInput

class Config:
    def __init__(self, default_user: str = "test"):
        self.default_user = default_user

class UserService:
    def __init__(self, config: Config):
        self.config = config

    def create_user(self, name: str) -> str:
        return f"Created user: {name}"

module = ModuleDef()
module.make(Config).using().type(Config)
module.make(UserService).using().type(UserService)

injector = Injector()
planner_input = PlannerInput([module])

# Pattern 1: Plan + Locator (most control)
plan = injector.plan(planner_input)
locator = injector.produce(plan)
service = locator.get(UserService)

# Pattern 2: Function injection (recommended)
def business_logic(service: UserService, config: Config) -> str:
    return service.create_user(config.default_user)

result = injector.produce_run(planner_input, business_logic)

# Pattern 3: Simple get (for quick usage)
service = injector.get(planner_input, UserService)
```

## Architecture

Chibi Izumi follows these design principles from the original distage:

1. **Non-invasive design** - Your classes remain framework-free, just use regular constructors
2. **Compile-time safety** - Dependencies are validated at plan creation time
3. **Type-safe bindings** - Algebraic data structure ensures binding correctness
4. **Immutable bindings** - Bindings are defined once and cannot be modified
5. **Explicit dependency graph** - All dependencies are explicit and traceable
6. **Fail-fast validation** - Circular and missing dependencies are detected early
7. **Zero-configuration features** - Automatic logger injection, factory patterns

## Limitations

This is a working implementation with some simplifications compared to the full distage library:

- Forward references in type hints have limited support
- No advanced lifecycle management (startup/shutdown hooks)
- Simplified error messages compared to Scala version
- No compile-time dependency graph visualization tools

## Contributing

This project was developed through AI-assisted programming with thorough manual review. Contributions, bug reports, and feedback are welcome!
