Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Service lifecycle hooks (initializers and finalizers)
- Task deduplication for diamond dependency patterns
- Boolean-based scope API with `scoped` and `transient` flags
- Circular dependency detection at build time with descriptive error messages showing the cycle path

### Changed

Expand Down
57 changes: 36 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# hdmi - Dependency Management Interface

> **Warning: Pre-Alpha Software**
>
> hdmi is experimental software in active development. Breaking changes may occur
> until version 1.0. Use with care in production environments.

A lightweight dependency injection framework for Python 3.13+ with:

- **Type-driven dependency discovery** - Uses Python's standard type annotations
Expand All @@ -12,6 +17,7 @@ A lightweight dependency injection framework for Python 3.13+ with:
### Simple Example (Singleton Services)

```python
import asyncio
from hdmi import ContainerBuilder

# Define your services
Expand All @@ -27,34 +33,43 @@ class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo

# Configure the container (all singletons by default)
builder = ContainerBuilder()
builder.register(DatabaseConnection)
builder.register(UserRepository)
builder.register(UserService)
async def main():
# Configure the container (all singletons by default)
builder = ContainerBuilder()
builder.register(DatabaseConnection)
builder.register(UserRepository)
builder.register(UserService)

# Build validates the dependency graph
container = builder.build()
# Build validates the dependency graph
container = builder.build()

# Resolve services lazily - dependencies are auto-wired!
user_service = container.get(UserService)
# Resolve services lazily - dependencies are auto-wired!
user_service = await container.get(UserService)

asyncio.run(main())
```

### Using Scoped Services

```python
# For request-scoped services (e.g., web requests)
builder = ContainerBuilder()
builder.register(DatabaseConnection) # singleton (default)
builder.register(UserRepository, scoped=True) # One per request
builder.register(UserService, transient=True) # New each time
import asyncio
from hdmi import ContainerBuilder

async def main():
# For request-scoped services (e.g., web requests)
builder = ContainerBuilder()
builder.register(DatabaseConnection) # singleton (default)
builder.register(UserRepository, scoped=True) # One per request
builder.register(UserService, transient=True) # New each time

container = builder.build()

container = builder.build()
# Scoped services must be resolved within a scope context
async with container.scope() as scoped:
user_service = await scoped.get(UserService)
# All scoped dependencies share the same instance within this scope

# Scoped services must be resolved within a scope context
with container.scope() as scoped:
user_service = scoped.get(UserService)
# All scoped dependencies share the same instance within this scope
asyncio.run(main())
```

## Key Features
Expand All @@ -77,12 +92,12 @@ Services have four lifecycles that are validated at build time:
The only invalid dependency is when a non-scoped service (singleton or transient) depends on a scoped service.

```python
#  Valid: Scoped Singleton
# Valid: Scoped -> Singleton
builder = ContainerBuilder()
builder.register(DatabaseConnection) # singleton (default)
builder.register(UserRepository, scoped=True)

# L Invalid: Singleton Scoped (raises ScopeViolationError)
# Invalid: Singleton -> Scoped (raises ScopeViolationError)
builder = ContainerBuilder()
builder.register(RequestHandler, scoped=True)
builder.register(SingletonService) # singleton depends on scoped!
Expand Down
6 changes: 3 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The documentation is organized into four categories based on the Diátaxis frame
- Focused on accomplishing specific tasks
- Assumes basic knowledge
- Practical and actionable
- Example: "How to configure YAML-based dependency injection"
- Example: "How to use service definitions with factories"

### 📖 Reference (`reference/`)
**Information-oriented**: Technical descriptions
Expand Down Expand Up @@ -57,8 +57,8 @@ make docs
# Build and watch for changes (auto-reload in browser)
make docs-watch

# Clean build artifacts
make docs-clean
# Clean build artifacts (includes docs)
make clean
```

The built documentation will be in `docs/_build/html/`. Open `docs/_build/html/index.html` in your browser to view it.
Expand Down
96 changes: 53 additions & 43 deletions docs/explanation/architecture.rst
Original file line number Diff line number Diff line change
Expand Up @@ -97,23 +97,27 @@ validated dependency graph ready for runtime use.

.. code-block:: python

# ContainerBuilder validates during .build()
container = builder.build()
# ↑ All validation happens HERE, by the ContainerBuilder
# - Dependency graph constructed and validated
# - Cycles checked
# - Scope hierarchy validated
# - Type compatibility ensured

# Container is now immutable and validated
# At this point:
# - All dependencies are validated ✓
# - No cycles exist ✓
# - Scope hierarchy is correct ✓
# - No services are instantiated yet

# Later, at runtime, Container just resolves:
user_service = container.get(UserService) # Lazy instantiation
import asyncio

async def main():
# ContainerBuilder validates during .build()
async with builder.build() as container:
# ↑ All validation happens HERE, by the ContainerBuilder
# - Dependency graph constructed and validated
# - Cycles checked
# - Scope hierarchy validated

# Container is now immutable and validated
# At this point:
# - All dependencies are validated ✓
# - No cycles exist ✓
# - Scope hierarchy is correct ✓
# - No services are instantiated yet

# Later, at runtime, Container just resolves:
user_service = await container.get(UserService) # Lazy instantiation

asyncio.run(main())

Service resolution flow
-----------------------
Expand Down Expand Up @@ -353,25 +357,30 @@ Example with Scopes:

.. code-block:: python

# Singleton: Created once
db = container.get(Database)
db2 = container.get(Database)
assert db is db2 # Same instance
import asyncio

async def main():
# Singleton: Created once
db = await container.get(Database)
db2 = await container.get(Database)
assert db is db2 # Same instance

# Scoped: Created once per scope
async with container.scope() as scoped:
handler1 = await scoped.get(RequestHandler)
handler2 = await scoped.get(RequestHandler)
assert handler1 is handler2 # Same instance within scope

# Scoped: Created once per scope
with container.create_scope() as scope:
handler1 = scope.get(RequestHandler)
handler2 = scope.get(RequestHandler)
assert handler1 is handler2 # Same instance within scope
async with container.scope() as scoped2:
handler3 = await scoped2.get(RequestHandler)
assert handler1 is not handler3 # Different instance in different scope

with container.create_scope() as scope2:
handler3 = scope2.get(RequestHandler)
assert handler1 is not handler3 # Different instance in different scope
# Transient: New instance every time
cmd1 = await container.get(CommandProcessor)
cmd2 = await container.get(CommandProcessor)
assert cmd1 is not cmd2 # Always different instances

# Transient: New instance every time
cmd1 = container.get(CommandProcessor)
cmd2 = container.get(CommandProcessor)
assert cmd1 is not cmd2 # Always different instances
asyncio.run(main())

Design principles
-----------------
Expand Down Expand Up @@ -442,7 +451,7 @@ Design philosophy
- **Python-native configuration**: Use type annotations, no external DSLs
- **Two-phase architecture**: Clear separation between configuration and runtime
- **Build-time validation**: Catch all configuration errors before runtime
- **Minimal dependencies**: Standard library only, no external packages
- **Minimal dependencies**: Only requires anyio for async support
- **Type-driven**: Leverage Python's typing system for safety and IDE support
- **Explicit over implicit**: Clear phase transitions and error messages

Expand All @@ -456,28 +465,29 @@ Error handling
Build -->|Success| Container[Container Created]
Build -->|Cycle Detected| CycleError[CircularDependencyError]
Build -->|Missing Dependency| MissingError[UnresolvableDependencyError]
Build -->|Type Mismatch| TypeError[TypeResolutionError]
Build -->|Scope Violation| ScopeError[ScopeViolationError]

Container --> Get{get}
Get -->|Success| Instance[Service Instance]
Get -->|Instantiation Error| InstError[InstantiationError]
Get -->|Not Registered| GetError[UnresolvableDependencyError]
Get -->|Scoped from Container| ScopeGetError[ScopeViolationError]

style CycleError fill:#ffcccc
style MissingError fill:#ffcccc
style TypeError fill:#ffcccc
style ScopeError fill:#ffcccc
style InstError fill:#ffcccc
style GetError fill:#ffcccc
style ScopeGetError fill:#ffcccc

All configuration errors are detected at **build time**, not at runtime:

- **CircularDependencyError**: Service A depends on B, which depends on A
- **UnresolvableDependencyError**: Required dependency is not registered
- **TypeResolutionError**: Type annotation cannot be resolved
- **ScopeViolationError**: Service depends on a service with shorter lifetime (e.g., singleton → scoped)
- **ScopeViolationError**: Non-scoped service depends on scoped service

Only instantiation errors occur at resolution time:
Runtime errors (when calling ``.get()``):

- **InstantiationError**: Constructor raised an exception
- **UnresolvableDependencyError**: Service type not registered in container
- **ScopeViolationError**: Attempting to resolve scoped service from root Container (use ``container.scope()`` instead)
- Standard Python exceptions if service constructor fails

This ensures "fail fast" behavior - catch issues early during setup, not in production.
This ensures "fail fast" behavior - catch configuration issues early during setup, not in production.
2 changes: 1 addition & 1 deletion docs/explanation/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,4 @@ Design Principles
3. **Type-Driven**: Python type annotations define dependencies
4. **No External DSL**: Pure Python, no YAML/XML configuration required
5. **Minimal Overhead**: Lightweight and fast
6. **Introspection First**: Easy to inspect and debug the dependency graph
6. **Async-First**: All service resolution is async for modern Python applications
Loading
Loading