Metadata-Version: 2.4
Name: philiprehberger-di
Version: 0.5.0
Summary: Lightweight dependency injection container for Python.
Project-URL: Homepage, https://github.com/philiprehberger/py-di#readme
Project-URL: Repository, https://github.com/philiprehberger/py-di
Project-URL: Issues, https://github.com/philiprehberger/py-di/issues
Project-URL: Changelog, https://github.com/philiprehberger/py-di/blob/main/CHANGELOG.md
Author: Philip Rehberger
License-Expression: MIT
License-File: LICENSE
Keywords: container,dependency-injection,di,inversion,ioc
Classifier: Development Status :: 3 - Alpha
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: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# philiprehberger-di

[![Tests](https://github.com/philiprehberger/py-di/actions/workflows/publish.yml/badge.svg)](https://github.com/philiprehberger/py-di/actions/workflows/publish.yml)
[![PyPI version](https://img.shields.io/pypi/v/philiprehberger-di.svg)](https://pypi.org/project/philiprehberger-di/)
[![Last updated](https://img.shields.io/github/last-commit/philiprehberger/py-di)](https://github.com/philiprehberger/py-di/commits/main)

Lightweight dependency injection container for Python.

## Installation

```bash
pip install philiprehberger-di
```

## Usage

```python
from philiprehberger_di import Container

container = Container()
container.register(Logger)
logger = container.resolve(Logger)
```

### Singletons

```python
container.register(Database, singleton=True)

a = container.resolve(Database)
b = container.resolve(Database)
assert a is b
```

### Custom Factories

```python
container.register(Cache, factory=lambda: Cache(max_size=256))
cache = container.resolve(Cache)
```

### Recursive Resolution

```python
class Service:
    def __init__(self, db: Database, logger: Logger) -> None:
        self.db = db
        self.logger = logger

container.register(Database)
container.register(Logger)
container.register(Service)
service = container.resolve(Service)  # db and logger are injected automatically
```

### Inject Decorator

```python
from philiprehberger_di import Container, inject

container = Container()
container.register(Logger, singleton=True)

@inject(container)
def handle_request(logger: Logger) -> str:
    logger.log("request handled")
    return "ok"

handle_request()  # logger is resolved and injected automatically
```

### Lifecycle Hooks

```python
container.register(
    Database,
    singleton=True,
    on_create=lambda db: db.connect(),
    on_destroy=lambda db: db.disconnect(),
)

db = container.resolve(Database)  # on_create called after creation
container.reset()                 # on_destroy called before clearing singletons
```

### Circular Dependency Detection

The container detects circular dependencies during resolution and raises a
`CircularDependencyError` with the full dependency chain:

```python
from philiprehberger_di import CircularDependencyError

class A:
    def __init__(self, b: B) -> None: ...

class B:
    def __init__(self, a: A) -> None: ...

container.register(A)
container.register(B)

try:
    container.resolve(A)
except CircularDependencyError as e:
    print(e)  # Circular dependency detected: A -> B -> A
    print(e.chain)  # [A, B, A]
```

### Scoped Lifetime

Services registered with `Lifetime.SCOPED` are singletons within a scope but
differ across scopes:

```python
from philiprehberger_di import Container, Lifetime

container = Container()
container.register(RequestContext, lifetime=Lifetime.SCOPED)
container.register(Logger, lifetime=Lifetime.SINGLETON)

with container.create_scope() as scope:
    ctx1 = scope.resolve(RequestContext)
    ctx2 = scope.resolve(RequestContext)
    assert ctx1 is ctx2  # same within the scope

# on_destroy hooks are called when the scope exits
```

### Lazy Resolution

Defer construction of an expensive service until first use:

```python
from philiprehberger_di import Container, Lifetime

container = Container()
container.register(ExpensiveClient, lifetime=Lifetime.SINGLETON)

proxy = container.lazy(ExpensiveClient)
# ExpensiveClient is NOT yet constructed

result = proxy.do_thing()  # constructed on first access, cached afterwards
```

### Introspection

Check whether a type is registered, or remove it. Useful when wiring up tests:

```python
container.is_registered(Database)  # True / False

container.register(Database, singleton=True)
container.unregister(Database)     # also clears cached singleton + calls on_destroy
```

## API

| Function / Class | Description |
|------------------|-------------|
| `Container()` | Create a new dependency injection container |
| `container.register(cls, factory?, singleton?, lifetime?, on_create?, on_destroy?)` | Register a class with optional factory, lifetime, and lifecycle hooks |
| `container.unregister(cls)` | Remove a registration; clears any cached singleton and calls `on_destroy` |
| `container.is_registered(cls)` | Return whether *cls* is registered in this container |
| `container.resolve(cls)` | Resolve an instance, recursively injecting dependencies |
| `container.lazy(cls)` | Return a `Lazy[cls]` proxy that resolves on first use |
| `container.create_scope()` | Create a child scope for scoped lifetime management |
| `container.reset()` | Call `on_destroy` for singletons with hooks, then clear the cache |
| `inject(container)` | Decorator that resolves type-hinted params from the container |
| `Lifetime.TRANSIENT` | New instance on every resolve (default) |
| `Lifetime.SINGLETON` | Single shared instance across the container |
| `Lifetime.SCOPED` | Single instance per scope, transient without a scope |
| `CircularDependencyError` | Raised when a circular dependency chain is detected |
| `Scope` | Child scope returned by `create_scope()`, used as a context manager |
| `Lazy[T]` | Proxy that resolves the underlying service on first access |

## Development

```bash
pip install -e .
python -m pytest tests/ -v
```

## Support

If you find this project useful:

⭐ [Star the repo](https://github.com/philiprehberger/py-di)

🐛 [Report issues](https://github.com/philiprehberger/py-di/issues?q=is%3Aissue+is%3Aopen+label%3Abug)

💡 [Suggest features](https://github.com/philiprehberger/py-di/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)

❤️ [Sponsor development](https://github.com/sponsors/philiprehberger)

🌐 [All Open Source Projects](https://philiprehberger.com/open-source-packages)

💻 [GitHub Profile](https://github.com/philiprehberger)

🔗 [LinkedIn Profile](https://www.linkedin.com/in/philiprehberger)

## License

[MIT](LICENSE)
