Metadata-Version: 2.4
Name: lucid-container
Version: 0.1.2
Summary: Dependency injection container with autowiring for Python.
Project-URL: Homepage, https://github.com/sharik709/lucid-container
Project-URL: Documentation, https://github.com/sharik709/lucid-container#readme
Project-URL: Repository, https://github.com/sharik709/lucid-container
Project-URL: Issues, https://github.com/sharik709/lucid-container/issues
Author-email: Sharik Shaikh <shaikhsharik709@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: autowiring,container,dependency-injection,di,inversion-of-control,ioc,service-provider
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: mypy>=1.0; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.1; extra == 'dev'
Description-Content-Type: text/markdown

# Lucid Container

**Dependency injection that wires itself. Bind abstracts to concretes, swap implementations by config, and let the container figure out the rest.**

Stop manually constructing dependency trees. Stop passing 12 arguments through 4 layers of functions. Stop refactoring every call site when you swap Redis for Memcached. Lucid Container gives Python a proper IoC container with autowiring — the kind of DI that gets out of your way.

[![PyPI version](https://badge.fury.io/py/lucid-container.svg)](https://pypi.org/project/lucid-container/)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)

---

## Before & After

**Without Lucid Container:**

```python
config = load_config()
logger = FileLogger(config["log_path"])
redis = RedisClient(config["redis_host"], config["redis_port"])
cache = RedisCache(redis, prefix=config["cache_prefix"])
mailer = SMTPMailer(config["smtp_host"], config["smtp_port"], logger)
user_repo = UserRepository(cache, logger)
auth_service = AuthService(user_repo, mailer, cache, logger)
payment_service = PaymentService(auth_service, logger, cache)
order_service = OrderService(payment_service, user_repo, mailer, cache, logger)

# And now do this again in tests with different implementations...
```

**With Lucid Container:**

```python
from lucid_container import Container

app = Container()
app.singleton(CacheContract, RedisCache)
app.singleton(MailerContract, SMTPMailer)
app.singleton(LoggerContract, FileLogger)

# Container reads type hints, builds the entire tree automatically
order_service = app.make(OrderService)

# Swap to in-memory for tests — one line, zero refactoring
app.singleton(CacheContract, InMemoryCache)
```

---

## Installation

```bash
pip install lucid-container
```

Requires Python 3.10 or higher. No dependencies.

---

## Quick Start

### Basic binding and resolution

```python
from lucid_container import Container

app = Container()

# Bind an abstract (interface) to a concrete (implementation)
app.bind(CacheContract, RedisCache)

# Resolve — container inspects RedisCache.__init__ type hints,
# recursively resolves dependencies, and returns a fully constructed instance
cache = app.make(CacheContract)
```

### Singletons

```python
# Same instance every time
app.singleton(LoggerContract, FileLogger)

logger1 = app.make(LoggerContract)
logger2 = app.make(LoggerContract)
assert logger1 is logger2  # True
```

### Autowiring (zero config)

```python
class OrderService:
    def __init__(self, payments: PaymentContract, mailer: MailerContract):
        self.payments = payments
        self.mailer = mailer

# If PaymentContract and MailerContract are bound, this just works.
# The container reads __init__ type hints and resolves each one.
order_service = app.make(OrderService)
```

---

## Full API Reference

### `Container()`

Creates a new container instance.

```python
from lucid_container import Container

app = Container()
```

---

### `.bind(abstract, concrete)`

Registers a binding. Every call to `make(abstract)` creates a **new instance**.

| Parameter  | Type                           | Description                              |
|------------|--------------------------------|------------------------------------------|
| `abstract` | `type \| str`                  | The interface, base class, or string key.|
| `concrete` | `type \| Callable[[Container], Any]` | A class to autowire, or a factory callable that receives the container. |

```python
# Class binding — container autowires the constructor
app.bind(CacheContract, RedisCache)

# Factory binding — you control construction
app.bind(CacheContract, lambda c: RedisCache(
    host=c.make("config")["redis_host"],
    port=c.make("config")["redis_port"],
))
```

---

### `.singleton(abstract, concrete)`

Same as `.bind()`, but the instance is created once and cached. Every subsequent `make()` returns the same object.

```python
app.singleton(DatabaseContract, PostgresDatabase)

db1 = app.make(DatabaseContract)
db2 = app.make(DatabaseContract)
assert db1 is db2
```

---

### `.instance(abstract, obj)`

Registers an existing object as a singleton. Useful for config objects, pre-built clients, or test doubles.

```python
config = {"debug": True, "db_host": "localhost"}
app.instance("config", config)
app.instance(LoggerContract, my_custom_logger)
```

---

### `.alias(name, abstract)`

Creates a shorthand name for an abstract.

```python
app.singleton(CacheContract, RedisCache)
app.alias("cache", CacheContract)

# Both return the same thing
app.make(CacheContract)
app.make("cache")
```

---

### `.make(abstract)`

Resolves an abstract to a fully constructed instance. This is the core method.

| Parameter  | Type           | Description                         |
|------------|----------------|-------------------------------------|
| `abstract` | `type \| str`  | What to resolve.                    |

**Resolution order:**

1. Check aliases — if `abstract` is a string alias, resolve to the canonical abstract.
2. Check singleton cache — if already resolved as a singleton, return cached instance.
3. Check bindings — if a binding exists, use it.
4. Autowire — if `abstract` is a concrete class (not an ABC), attempt to autowire it directly.
5. Raise `ResolutionError` if nothing works.

```python
cache = app.make(CacheContract)
config = app.make("config")
```

---

### `.has(abstract)`

Returns `True` if the abstract is bound, aliased, or has a cached singleton.

```python
app.bind(CacheContract, RedisCache)
app.has(CacheContract)  # True
app.has(MailerContract)  # False
```

---

### `.flush()`

Clears all bindings, singletons, and aliases. Useful in tests.

```python
app.flush()
```

---

### `.bound_to(abstract)`

Returns the concrete class or factory bound to an abstract, or `None`.

```python
app.bind(CacheContract, RedisCache)
app.bound_to(CacheContract)  # RedisCache
```

---

### Autowiring

The container's most powerful feature. When resolving a class, the container:

1. Inspects `__init__` using `inspect.signature()` and `typing.get_type_hints()`.
2. For each parameter with a type annotation that's bound in the container, recursively calls `make()`.
3. For parameters with default values and no binding, uses the default.
4. For parameters without a type hint or binding, raises `ResolutionError`.

```python
class NotificationService:
    def __init__(
        self,
        mailer: MailerContract,        # resolved from container
        cache: CacheContract,          # resolved from container
        retries: int = 3,              # uses default (not in container)
    ):
        self.mailer = mailer
        self.cache = cache
        self.retries = retries

# Works automatically — mailer and cache are resolved, retries uses default
service = app.make(NotificationService)
```

Autowiring also works for **concrete classes that aren't explicitly bound**:

```python
# UserService is never registered — but its dependencies are.
# The container can still build it.
class UserService:
    def __init__(self, repo: UserRepository, logger: LoggerContract):
        ...

service = app.make(UserService)  # Just works
```

---

### Contextual Binding

Sometimes the same abstract should resolve to different concretes depending on *who's asking*.

```python
app.when(PhotoController).needs(StorageContract).give(S3Storage)
app.when(ReportController).needs(StorageContract).give(LocalStorage)

# When building PhotoController, StorageContract → S3Storage
# When building ReportController, StorageContract → LocalStorage
photo = app.make(PhotoController)
report = app.make(ReportController)
```

---

### Tagging

Group related bindings under a tag, then resolve them all at once.

```python
app.bind("reports.daily", DailySalesReport)
app.bind("reports.weekly", WeeklyInventoryReport)
app.bind("reports.monthly", MonthlyRevenueReport)

app.tag(["reports.daily", "reports.weekly", "reports.monthly"], "reports")

# Resolve all tagged bindings
all_reports = app.tagged("reports")
# Returns: [DailySalesReport(...), WeeklyInventoryReport(...), MonthlyRevenueReport(...)]
```

---

### Container Events

Hook into resolution for logging, decoration, or post-construction setup.

#### `.resolving(abstract, callback)`

Called every time `abstract` is resolved, **before** returning. Receives the resolved instance and the container. Return value replaces the instance.

```python
def add_logging(instance, container):
    instance.logger = container.make(LoggerContract)
    return instance

app.resolving(PaymentService, add_logging)
```

#### `.after_resolving(abstract, callback)`

Called after the instance is returned. Useful for non-mutating side effects like event emission or metrics.

```python
app.after_resolving(PaymentService, lambda inst, c: metrics.increment("payment_service.resolved"))
```

#### Global resolving (no abstract filter)

```python
# Called for EVERY resolution
app.resolving_any(lambda instance, container: print(f"Resolved: {type(instance).__name__}"))
```

---

### Method Injection

Resolve dependencies for a function call, not just constructor injection.

```python
def send_welcome(user_id: int, mailer: MailerContract, cache: CacheContract):
    user = cache.get(f"user:{user_id}")
    mailer.send(user.email, "Welcome!")

# Container injects mailer and cache, you provide user_id
app.call(send_welcome, {"user_id": 42})
```

---

## Service Providers

Service providers are the **recommended** way to organize bindings. Each provider is responsible for one subsystem — cache, mail, queue, logging, etc.

### Base class

```python
from lucid_container import ServiceProvider

class CacheServiceProvider(ServiceProvider):

    def register(self):
        """Bind into the container. Called first on ALL providers."""
        self.app.singleton(CacheContract, lambda c: self._create_cache(c))

    def boot(self):
        """Called after ALL providers have registered. Safe to resolve."""
        cache = self.app.make(CacheContract)
        cache.set("app.booted", True)

    def _create_cache(self, container):
        config = container.make("config").get("cache", {})
        driver = config.get("driver", "memory")

        if driver == "redis":
            return RedisCache(
                host=config.get("host", "localhost"),
                port=config.get("port", 6379),
            )
        elif driver == "file":
            return FileCache(path=config.get("path", "/tmp/cache"))
        else:
            return InMemoryCache()
```

### Registering providers

```python
app = Container()
app.instance("config", load_config())

app.register_provider(CacheServiceProvider)
app.register_provider(MailServiceProvider)
app.register_provider(QueueServiceProvider)

# register() is called on each immediately.
# boot() is called on all providers after all register() calls complete.
app.boot()
```

### Provider lifecycle

1. `app.register_provider(P)` → instantiates `P(app)` and calls `P.register()`.
2. `app.boot()` → calls `boot()` on every registered provider, in order.
3. After boot, the container is fully ready.

This two-phase lifecycle is critical: during `register()`, providers can only bind. During `boot()`, providers can safely resolve from the container because all bindings exist.

---

## Driver Manager

For subsystems with swappable backends (cache, queue, mail, storage, etc.), use the `DriverManager` base class.

```python
from lucid_container import DriverManager

class CacheManager(DriverManager):
    """Manages cache driver instantiation and switching."""

    def create_redis_driver(self, config: dict):
        return RedisCache(host=config["host"], port=config["port"])

    def create_memory_driver(self, config: dict):
        return InMemoryCache()

    def create_file_driver(self, config: dict):
        return FileCache(path=config["path"])

    def get_default_driver(self) -> str:
        return self.config.get("default", "memory")
```

### How DriverManager works

- Subclass names driver creators as `create_{name}_driver(self, config)`.
- `get_default_driver()` returns the name of the default driver from config.
- `.driver(name=None)` returns a cached driver instance. `None` uses the default.
- `.extend(name, creator)` registers custom drivers at runtime.

```python
# In a service provider:
def register(self):
    self.app.singleton(CacheManager, lambda c: CacheManager(
        container=c,
        config=c.make("config").get("cache", {}),
    ))
    self.app.bind(CacheContract, lambda c: c.make(CacheManager).driver())
    self.app.alias("cache", CacheContract)

# Usage — swap drivers by config:
config = {"cache": {"default": "redis", "drivers": {"redis": {"host": "localhost", "port": 6379}}}}

# Or on the fly:
file_cache = app.make(CacheManager).driver("file")

# Third-party packages can extend:
app.make(CacheManager).extend("dynamodb", lambda config: DynamoDBCache(**config))
```

---

## Real-World Examples

### Web application bootstrap

```python
from lucid_container import Container

app = Container()

# Config
app.instance("config", {
    "cache": {"default": "redis", "drivers": {"redis": {"host": "localhost"}}},
    "mail": {"default": "smtp", "drivers": {"smtp": {"host": "mail.example.com"}}},
    "database": {"host": "localhost", "name": "myapp"},
})

# Register providers
app.register_provider(LoggingServiceProvider)
app.register_provider(DatabaseServiceProvider)
app.register_provider(CacheServiceProvider)
app.register_provider(MailServiceProvider)

app.boot()

# In your request handler — the entire tree resolves automatically
controller = app.make(UserController)
response = controller.handle(request)
```

### Testing with swapped implementations

```python
def test_order_processing():
    app = Container()

    # Bind test doubles
    app.instance(CacheContract, InMemoryCache())
    app.instance(MailerContract, FakeMailer())
    app.instance(PaymentContract, FakePaymentGateway(always_succeeds=True))

    # Everything downstream uses the fakes automatically
    service = app.make(OrderService)
    result = service.process(order)

    assert result.status == "completed"
    assert app.make(MailerContract).last_sent.subject == "Order Confirmed"
```

### Contextual binding for multi-tenant

```python
app.when(TenantAController).needs(DatabaseContract).give(
    lambda c: PostgresDatabase(host="tenant-a.db.example.com")
)
app.when(TenantBController).needs(DatabaseContract).give(
    lambda c: PostgresDatabase(host="tenant-b.db.example.com")
)
```

---

## Architecture

### Project Structure

```
lucid-container/
├── src/
│   └── lucid_container/
│       ├── __init__.py             # Public API exports
│       ├── container.py            # Container class
│       ├── service_provider.py     # ServiceProvider base class
│       ├── driver_manager.py       # DriverManager base class
│       ├── contextual_binding.py   # ContextualBindingBuilder
│       ├── autowire.py             # Autowiring logic (inspect + type hints)
│       └── exceptions.py           # ResolutionError, BindingError
├── tests/
│   ├── __init__.py
│   ├── test_binding.py             # bind, singleton, instance
│   ├── test_autowire.py            # Autowiring from type hints
│   ├── test_aliases.py             # Alias resolution
│   ├── test_contextual.py          # Contextual bindings
│   ├── test_tagging.py             # Tag and tagged()
│   ├── test_events.py              # resolving / after_resolving hooks
│   ├── test_method_injection.py    # container.call()
│   ├── test_service_provider.py    # Provider lifecycle
│   ├── test_driver_manager.py      # DriverManager pattern
│   ├── test_flush.py               # flush() and re-binding
│   └── test_edge_cases.py          # Circular deps, missing bindings, etc.
├── pyproject.toml
├── README.md
├── LICENSE
└── CHANGELOG.md
```

### Implementation Notes

**Container internals:**

The container maintains five internal data structures:

```python
self._bindings: dict[Any, Callable]        # abstract → factory or class
self._singletons: dict[Any, Any]           # abstract → cached instance (None = not yet resolved)
self._aliases: dict[str, Any]              # string alias → abstract
self._tags: dict[str, list[Any]]           # tag name → list of abstracts
self._contextual: dict[type, dict[type, Any]]  # consumer → {abstract → concrete}
```

**Autowiring algorithm:**

```python
def _autowire(self, concrete: type) -> Any:
    hints = typing.get_type_hints(concrete.__init__)
    sig = inspect.signature(concrete.__init__)
    kwargs = {}

    for name, param in sig.parameters.items():
        if name == "self":
            continue

        hint = hints.get(name)

        # 1. Check contextual bindings for the current build stack
        # 2. If hint is bound in container → recursive make()
        # 3. If param has a default → skip (use default)
        # 4. Otherwise → raise ResolutionError

    return concrete(**kwargs)
```

The container maintains a **build stack** (a list of types currently being constructed) to detect circular dependencies. If type A requires B and B requires A, the container raises `CircularDependencyError` with the full cycle path instead of infinite recursion.

**Contextual binding resolution:**

During autowiring, the container checks the build stack. If the class currently being built has a contextual override for the dependency being resolved, that override takes priority over the global binding.

**Factory vs class binding:**

- If `concrete` is a callable but not a class → treated as a factory. Called with `concrete(container)`.
- If `concrete` is a class → autowired. The container inspects `__init__` and builds it.
- This is determined by `inspect.isclass(concrete)`.

**Thread safety:**

The container is NOT thread-safe. In threaded applications, create one container at boot and treat it as read-only after `boot()`. If you need per-request scoping, create a child container (see scoped containers below).

**Scoped containers (child containers):**

```python
request_container = app.scoped()
request_container.instance("request", current_request)

# Resolves "request" from child, everything else falls through to parent
controller = request_container.make(UserController)
```

A scoped container inherits all bindings from the parent but has its own singleton cache. Bindings added to the child don't affect the parent. This is how per-request scoping works.

### Public API (what `__init__.py` exports)

```python
from lucid_container.container import Container
from lucid_container.service_provider import ServiceProvider
from lucid_container.driver_manager import DriverManager
from lucid_container.exceptions import ResolutionError, BindingError, CircularDependencyError

__all__ = [
    "Container",
    "ServiceProvider",
    "DriverManager",
    "ResolutionError",
    "BindingError",
    "CircularDependencyError",
]
```

### Exceptions

| Exception                  | When                                                              |
|----------------------------|-------------------------------------------------------------------|
| `ResolutionError`          | Cannot resolve an abstract — no binding, no autowiring possible.  |
| `BindingError`             | Invalid binding (e.g., binding to a non-callable).                |
| `CircularDependencyError`  | A depends on B depends on A. Includes the cycle path.             |

---

## pyproject.toml Specification

```toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "lucid-container"
version = "0.1.0"
description = "Dependency injection container with autowiring for Python."
readme = "README.md"
license = "MIT"
requires-python = ">=3.10"
authors = [
    { name = "Your Name", email = "your@email.com" },
]
keywords = [
    "dependency-injection", "ioc", "container", "di",
    "autowiring", "service-provider", "inversion-of-control",
]
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Programming Language :: Python :: 3.13",
    "Topic :: Software Development :: Libraries :: Python Modules",
    "Typing :: Typed",
]

[project.urls]
Homepage = "https://github.com/yourname/lucid-container"
Documentation = "https://github.com/yourname/lucid-container#readme"
Repository = "https://github.com/yourname/lucid-container"
Issues = "https://github.com/yourname/lucid-container/issues"

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.mypy]
strict = true

[project.optional-dependencies]
dev = ["pytest>=7.0", "mypy>=1.0", "ruff>=0.1"]
```

---

## Test Cases to Implement

### Binding

- `bind()` registers a class, `make()` returns a new instance each time
- `bind()` with a factory callable, factory receives the container
- `singleton()` returns the same instance every call
- `instance()` stores and returns the exact object
- Re-binding overwrites the previous binding
- Re-binding a singleton clears the cached instance
- `has()` returns True for bound abstracts, False otherwise
- `bound_to()` returns the concrete or None
- `flush()` clears everything

### Autowiring

- Class with no `__init__` is instantiated directly
- Class with typed dependencies resolves each from container
- Class with default values uses defaults for unbound params
- Class with mixed bound and default params resolves correctly
- Unbound param without default raises `ResolutionError`
- Recursive autowiring — A needs B, B needs C, all resolved
- Concrete class not explicitly bound is still autowired if dependencies are available
- `*args` and `**kwargs` in `__init__` are ignored
- Parameters with `str`, `int`, `float`, `bool` hints are not autowired (primitives)

### Aliases

- `alias()` maps string to abstract
- `make()` with alias string resolves correctly
- Chained aliases resolve (alias → alias → abstract)
- `has()` returns True for aliases

### Contextual Binding

- `.when(A).needs(X).give(Y)` — when building A, X resolves to Y
- `.when(B).needs(X).give(Z)` — when building B, X resolves to Z
- Without contextual binding, global binding applies
- Contextual binding with factory callable
- Contextual binding works through nested resolution (A needs B needs X)

### Tagging

- `tag()` associates abstracts with a tag name
- `tagged()` resolves all tagged abstracts
- `tagged()` returns empty list for unknown tag
- Multiple tags on the same abstract

### Container Events

- `resolving()` callback is called on resolution with (instance, container)
- `resolving()` return value replaces the instance
- `after_resolving()` is called after resolution
- `resolving_any()` fires for every resolution
- Multiple callbacks on the same abstract all fire, in order

### Method Injection

- `call()` inspects function type hints and injects from container
- `call()` with explicit args merges with injected args
- `call()` works with both functions and bound methods
- Unresolvable params without explicit args raise `ResolutionError`

### Service Providers

- `register_provider()` calls `register()` immediately
- `boot()` calls `boot()` on all providers in order
- Provider's `self.app` is the container
- Multiple providers register without conflicts
- Provider boot can resolve bindings from other providers

### Driver Manager

- Subclass with `create_x_driver()` methods resolves drivers by name
- `driver()` with no args uses `get_default_driver()`
- `driver("x")` returns cached instance on second call
- `extend()` registers custom driver creator
- Unknown driver name raises `ResolutionError`

### Scoped Containers

- `scoped()` returns a child container
- Child resolves from parent bindings
- Child singletons are independent from parent singletons
- Bindings added to child don't leak to parent
- Parent changes after scoping don't affect existing child

### Edge Cases

- Circular dependency detection with clear error message including the cycle path
- Binding an abstract to itself (concrete class is the abstract)
- `make()` on a plain concrete class with no bindings (autowire directly)
- `make()` with `str`, `int`, or `None` hints doesn't try to autowire those
- Empty container — `make()` raises `ResolutionError`
- Container as a dependency — if a param is typed `Container`, inject the container itself
- Provider `register()` raising an exception doesn't break other providers
- `__init__` with `self` only (no params) resolves successfully

---

## Part of the Lucid Ecosystem

Lucid Container is the second package in the **Lucid** ecosystem — and the backbone that every future package builds on.

Released:

- `lucid-pipeline` — Clean, expressive pipelines for multi-step data processing.

Coming soon:

- `lucid-cache` — Multi-driver cache with a unified API, wired through the container.
- `lucid-config` — Cascading configuration management.
- `lucid-events` — Event dispatcher with listeners and subscribers.

---

## License

MIT License. See [LICENSE](LICENSE) for details.
