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
2 changes: 1 addition & 1 deletion docs/CONTRACT_BUDGET.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Keep only behavioral:
- `Result`, `ResultLike`, `SuccessCheckable`, `StructuredError`, `ErrorDomainProtocol`
- `Model` (structural), `Routable`, `Dispatcher`, `Handle`, `Execute`,
`AutoDiscoverableHandler`
- `Context`, `Container` (protocol, not the concrete class), `ProviderLike`
- `Context`, `Container` (protocol, not the concrete class)
- `Settings`, `Configurable`
- `Logger`, `OutputLogger`, `Flushable`
- `Registry`, `RegistryBacked`
Expand Down
6 changes: 3 additions & 3 deletions docs/architecture/clean-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ responsibilities.
│ │ loggings.py, container.py
├─────────────────────────────────────┤
│ L1: Foundation & Bridge │ result.py, exceptions.py, registry.py
│ (railway result, error surface) │ runtime.py (structlog/dependency-injector bridge)
│ (railway result, error surface) │ runtime.py (runtime normalization and validation)
├─────────────────────────────────────┤
│ L0: Pure Contracts │ constants.py, typings.py, protocols.py
│ (immutable constants & protocols) │
Expand Down Expand Up @@ -78,8 +78,8 @@ from flext_core import FlextDispatcher # not allowed inside result.py
- `exceptions.py` centralizes typed exceptions surfaced by dispatcher orchestration.
- `registry.py` shares low-level registration helpers reused by dispatcher and
container flows.
- `runtime.py` bridges structlog and dependency-injector while deliberately avoiding
imports from L2/L3 to prevent cycles.
- `runtime.py` normalizes runtime payloads and validates metadata while deliberately
avoiding imports from L2/L3 to prevent cycles.

- **L2 – Domain & Infrastructure**

Expand Down
10 changes: 5 additions & 5 deletions docs/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Canonical references:
┌─────────────────────────────────────────────────────────────┐
│ Foundation & Bridge Layers (L1) │
│ result.py, exceptions.py, registry.py │
│ runtime.py (structlog/dependency-injector bridge) │
│ runtime.py (runtime normalization and validation) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
Expand All @@ -64,8 +64,8 @@ Canonical references:

- **L1 – runtime bridge and results**

- `runtime.py` wraps structlog and dependency-injector factories so higher layers can
configure logging and DI without depending on third-party types.
- `runtime.py` normalizes runtime payloads and validates metadata so higher layers
never handle raw third-party types.
- `result.py` delivers the railway-oriented `r`; `exceptions.py` contains the CQRS
exception hierarchy consumed by handlers.
- `registry.py` offers the shared registration helpers reused by dispatcher,
Expand All @@ -79,8 +79,8 @@ Canonical references:
- Infrastructure sits beside the domain types: `_settings.py` (`FlextSettings` via
`BaseSettings`), `context.py` (contextvars metadata propagation), `loggings.py`
(`FlextUtilitiesLogging`), `utilities.py`/`_utilities/*` (validation, pagination,
caching, data mappers, reliability helpers), and `container.py`
(dependency-injector singleton plus scoped container factory).
caching, data mappers, reliability helpers), and `container.py` (the core runtime
registry: one validated write path, singleton plus scopes).

- **L3 – application orchestration**

Expand Down
2 changes: 1 addition & 1 deletion docs/development/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ src/flext_core/
├── _settings.py # Settings management
├── _config.py # Config management
├── loggings.py # Structured logging
├── runtime.py # structlog/dependency-injector bridge
├── runtime.py # Runtime normalization and validation
├── registry.py # Shared registration helpers
├── context.py # Contextvars metadata propagation
├── dispatcher.py # CQRS dispatch
Expand Down
126 changes: 81 additions & 45 deletions docs/guides/dependency-injection-advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

- [Overview](#overview)
- [Reusing Official Example Code](#reusing-official-example-code)
- [Registration Rules](#registration-rules)
- [Core Container Operations](#core-container-operations)
- [Scoped Containers](#scoped-containers)
- [Batch Registration Pattern](#batch-registration-pattern)
- [Factory Auto-Registration](#factory-auto-registration)
- [Best Practices](#best-practices)

<!-- TOC END -->
Expand All @@ -20,7 +21,9 @@ Services do not use the container. A service declares each collaborator as a por
(`t.Port[p.X]`) and the project's `api.py` passes adapters to its constructor; see
[Service Patterns](service-patterns.md). `FlextContainer` is the registry of the core
runtime (settings, context, command bus, logger) and the tool for infrastructure code
that composes that runtime.
that composes that runtime. It has no protocol-keyed binding and no string-keyed
`provide`/`wire` bridge: pure dependency injection through constructors is the
composition model.

## Reusing Official Example Code

Expand All @@ -33,75 +36,108 @@ demo = Ex08FlextContainer("docs/guides/dependency-injection-advanced.md")
demo.exercise()
```

The `Ex08FlextContainer` flow exercises binding, factories, resolution, and scoped
containers.
The `Ex08FlextContainer` flow exercises binding, factories, resources, the registration
rules, resolution, and scoped containers.

## Core Container Operations
## Registration Rules

```python
from flext_core import FlextContainer, u
The container keeps one mapping of services, factories and resources, and every write
(`bind`, `factory`, `resource`, a `m.ServiceRegistrationSpec`, a scope declaration or
factory auto-registration) passes one private write path. That path raises
`e.ValidationError` instead of ignoring the write when:

container = FlextContainer()
- the name is empty;
- the name is already registered, whatever its kind (service, factory or resource);
- the name is reserved for the core runtime (`c.CONTAINER_RESERVED_NAMES`: `settings`,
`logger`, `context`, `command_bus`);
- the value fails its record validation, for example a factory that is not callable.

_ = container.bind("settings_name", "flext-core")
_ = container.factory("logger", lambda: u.fetch_logger(__name__))
The reserved core services resolve normally but stay out of `has`, `names` and `drop`.
To replace a public registration, `drop` it first.

settings_name = container.resolve("settings_name")
logger = container.resolve("logger")

assert settings_name.success
assert logger.success
```python
from flext_core import FlextContainer, c, e

container = FlextContainer.shared().scope()
_ = container.bind("feature_flag", True)

try:
_ = container.bind("feature_flag", False)
except e.ValidationError as exc:
assert "feature_flag" in str(exc)
else:
raise AssertionError

try:
_ = container.bind(c.ServiceName.LOGGER, "not a logger")
except e.ValidationError as exc:
assert "reserved" in str(exc)
else:
raise AssertionError

assert container.resolve("feature_flag").value is True
assert container.resolve(c.ServiceName.LOGGER).success
assert not container.has(c.ServiceName.LOGGER)
```

## Scoped Containers
## Core Container Operations

```python
from flext_core import FlextContainer
from flext_core import FlextContainer, u

root = FlextContainer()
_ = root.bind("tenant", "default")
container = FlextContainer.shared().scope()

_ = container.bind("app_name", "flext-core")
_ = container.factory("module_logger", lambda: u.fetch_logger(__name__))

scoped = root.scope(subproject="tenant_a")
tenant = scoped.resolve("tenant")
app_name = container.resolve("app_name")
logger = container.resolve("module_logger")

assert tenant.success
assert tenant.value == "default"
assert app_name.value == "flext-core"
assert logger.success
assert set(container.names()) >= {"app_name", "module_logger"}
```

## Batch Registration Pattern
A factory and a resource are invoked on every `resolve`; a callable that raises, or
returns a value that is not a registerable service, yields a failed result carrying the
cause.

`FlextContainer` does not expose `batch_register`; use explicit loop registration for
deterministic failure points.
## Scoped Containers

```python
from __future__ import annotations
`scope(...)` builds an isolated container that inherits the public registrations of its
parent. Registrations declared by the scope's `m.ServiceRegistrationSpec` replace
inherited names, and the scope binds its own core services to its own settings and
context. Writes to the scope never reach the parent.

from flext_core import FlextContainer, p, r, t
```python
from flext_core import FlextContainer, m

root = FlextContainer.shared().scope()
_ = root.bind("tenant", "default")

def bind_services(
container: FlextContainer, services: t.SequenceOf[tuple[str, t.RegisterableService]]
) -> p.Result[bool]:
for name, service in services:
_ = container.bind(name, service)
resolved = container.resolve(name)
if resolved.failure:
return r[bool].from_failure(resolved)
return r[bool].ok(True)
scoped = root.scope(
subproject="tenant_a",
registration=m.ServiceRegistrationSpec(services={"tenant": "tenant_a"}),
)

assert scoped.resolve("tenant").value == "tenant_a"
assert root.resolve("tenant").value == "default"
assert scoped.resolve("settings").value is scoped.settings
assert scoped.context.get("subproject").value == "tenant_a"
```

container = FlextContainer()
result = bind_services(container, (("service_a", "ok"), ("service_b", "ok")))
## Factory Auto-Registration

assert result.success
```
`FlextContainer.shared(auto_register_factories=True)` registers every `@d.factory()`
function of the calling module through the same write path. A caller that cannot be
resolved to a module imported in `sys.modules` raises `e.ValidationError`; a duplicate
or reserved factory name raises as for any other write.

## Best Practices

- Keep service names stable and explicit.
- Keep service names stable and explicit; never reuse a name without `drop`.
- Prefer `bind` for concrete instances and `factory` for deferred construction.
- Validate each critical resolution step with `result.success`, and propagate a failure
with its cause (`r[T].from_failure(result)`).
- Let a registration error propagate: it names the rule the write broke.
- Use `scope(...)` for isolation when composing runtime contexts.
- Never resolve a service's collaborator from the container inside the service; pass it
as a port from the composition root.
9 changes: 6 additions & 3 deletions docs/guides/service-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,9 @@ else:
## Composition root

The project's `api.py` is the only module that builds adapters and passes them to
services (pure dependency injection). One adapter shared by two services is a variable
passed to both constructors:
services. Pure dependency injection through constructors is the definitive composition
model: there is no protocol-keyed container binding and no `compose()` step. One adapter
shared by two services is a variable passed to both constructors:

```python notest
from __future__ import annotations
Expand All @@ -134,7 +135,9 @@ audit = AuditService(clock=clock)
- `fetch_global()` builds the per-class singleton with no arguments, so it serves only
services without ports. A service with a required port raises `ValidationError` there.
- `FlextContainer` is the registry of the core runtime (settings, context, command bus,
logger). Services and adapters never call it.
logger). Services and adapters never call it. Its writes follow one rule path: an
empty, duplicate or reserved name raises `e.ValidationError`; see
[Dependency Injection Advanced](dependency-injection-advanced.md).

## Settings and the runtime hook

Expand Down
20 changes: 6 additions & 14 deletions examples/ex_08_container_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,6 @@ def _exercise_internal_and_cleanup(
) -> None:
"""Exercise lifecycle helpers and cleanup APIs."""
self.section("internal_and_cleanup")
container.initialize_di_components()
self.audit_check(
"initialize_di_components.bridge_exists", hasattr(container, "_di_bridge")
)
self.audit_check(
"initialize_di_components.container_exists",
hasattr(container, "_di_container"),
)
container.initialize_registrations(
registration=m.ServiceRegistrationSpec(
settings=root.settings.clone(), context=root.context
Expand All @@ -33,17 +25,17 @@ def _exercise_internal_and_cleanup(
self.audit_check(
"initialize_registrations.list_services_empty", len(container.names())
)
container.sync_config_to_di()
container.register_existing_providers()
container.register_core_services()
self.audit_check(
"sync_settings_to_di.service_settings_present", container.has("settings")
"core_services.settings_internal",
not container.has("settings") and container.resolve("settings").success,
)
self.audit_check(
"register_core_services.logger_present", container.has("logger")
"core_services.logger_internal",
not container.has("logger") and container.resolve("logger").success,
)
self.audit_check(
"register_core_services.command_bus_present", container.has("command_bus")
"core_services.command_bus_internal",
not container.has("command_bus") and container.dispatcher().success,
)
logger_default = container.logger(f"examples.{self.rand_str(6)}")
logger_custom = container.logger(f"examples.{self.rand_str(6)}")
Expand Down
Loading
Loading