From 53edfe9c4ccfaa07265bf7e7c8c59ace420192f3 Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Sat, 26 Sep 2026 13:03:46 -0300 Subject: [PATCH 1/6] feat(container): truthful container with one validated write path Empty, duplicate and reserved names raise e.ValidationError through one private write path over a single registrations mapping; reserved core names live in c.CONTAINER_RESERVED_NAMES. shared(auto_register_factories) raises when the caller module cannot be resolved, and scanned factories pass the validated factory record. The string-only dependency_injector bridge (provide, wire, the _di_* state and FlextRuntime.DependencyIntegration) is removed after a fleet-wide zero-use proof, and SkipValidation leaves the container registration models. scope() binds its own core services and keeps LOGGER internal. Co-Authored-By: Claude Opus 5.5 (1M context) --- .gitignore | 3 +- Makefile | 3 +- docs/CONTRACT_BUDGET.md | 2 +- docs/api-reference/generated/overview.md | 24 +- .../api-reference/generated/projects/index.md | 13 + docs/architecture/clean-architecture.md | 6 +- docs/architecture/overview.md | 8 +- docs/development/contributing.md | 2 +- docs/guides/dependency-injection-advanced.md | 126 ++-- docs/guides/service-patterns.md | 9 +- docs/projects/generated/catalog.md | 2 +- examples/ex_08_container_lifecycle.py | 20 +- examples/ex_08_container_registration.py | 115 ++- examples/ex_08_container_scoped.py | 51 +- examples/ex_08_flext_container.expected | 37 +- examples/ex_08_flext_container.py | 2 +- examples/ex_10_flext_handlers.expected | 116 --- mkdocs.yml | 9 +- pyproject.toml | 6 +- .../flextconstantserrors_part_05.py | 35 +- src/flext_core/_constants/infrastructure.py | 10 + .../flextmodelscontainer_part_01.py | 23 +- .../flextmodelscontainer_part_02.py | 6 +- .../flextmodelscontainer_part_03.py | 6 - .../flextprotocolscontainer_part_01.py | 54 -- .../flextprotocolscontainer_part_02.py | 18 - .../flextprotocolscontainer_part_03.py | 12 - src/flext_core/_runtime/__init__.py | 12 - src/flext_core/_runtime/_dependency.py | 19 - .../_runtime/_dependency_bindings.py | 182 ----- .../_runtime/_dependency_options.py | 65 -- src/flext_core/_runtime/_dependency_types.py | 68 -- src/flext_core/_utilities/model_runtime.py | 79 +- src/flext_core/container.py | 672 +++++++----------- src/flext_core/context.py | 13 +- src/flext_core/registry.py | 24 +- src/flext_core/runtime.py | 6 +- tests/_constants/other.py | 5 + tests/integration/test_integration.py | 8 +- tests/unit/test_container_lifecycle.py | 10 +- tests/unit/test_container_properties.py | 23 +- tests/unit/test_container_registration.py | 54 +- tests/unit/test_container_truthful.py | 155 ++++ tests/unit/test_lazy_exports.py | 8 +- tests/unit/test_runtime.py | 33 - tests/unit/test_service_registration_spec.py | 117 ++- uv.lock | 23 +- 47 files changed, 792 insertions(+), 1502 deletions(-) create mode 100644 docs/api-reference/generated/projects/index.md delete mode 100644 examples/ex_10_flext_handlers.expected delete mode 100644 src/flext_core/_runtime/_dependency.py delete mode 100644 src/flext_core/_runtime/_dependency_bindings.py delete mode 100644 src/flext_core/_runtime/_dependency_options.py delete mode 100644 src/flext_core/_runtime/_dependency_types.py create mode 100644 tests/unit/test_container_truthful.py diff --git a/.gitignore b/.gitignore index 2b8979b3c3..34a91cd5ee 100644 --- a/.gitignore +++ b/.gitignore @@ -36,9 +36,7 @@ */.hooks.json.agents-governance.json .claude/*.agents-governance.json .gemini/*.agents-governance.json -.claude/settings.json .claude/settings.local.json -.gemini/settings.json # Operator-private local config overrides /config/codegen-overrides.local.yaml @@ -359,6 +357,7 @@ CLAUDE.local.md !.github/scripts/ !.github/scripts/check-beads-policy.sh !.github/scripts/check-cqrs-compliance.sh +!.github/scripts/validate-git-hook.sh !.github/scripts/flext-law-loop.sh !tests/fixtures/ !tests/fixtures/ci/ diff --git a/Makefile b/Makefile index 909eb28d28..5d63f9eb8a 100644 --- a/Makefile +++ b/Makefile @@ -149,7 +149,8 @@ endif endif # End SECTION: REPOSITORY_ROOT isolation # === SECTION: verb dispatch (managed) === -# Source: config:make.verbs and the canonical gate vocabulary. +# Source: config:make.verbs and the canonical gate vocabulary. A verb exists +# only in the profiles it declares (make.verbs[].profiles). PUBLIC_VERBS := help setup upg build check test test-full fmt fix fix-enforcement audit status docs clean release-plan release-version release-tag release-build publication gen initialize mod waza duplication sonarcloud-sync BUILTIN_VERBS := help setup upg build check test test-full fmt fix fix-enforcement audit status docs clean release-plan release-version release-tag release-build publication gen initialize mod waza duplication sonarcloud-sync SCRIPT_VERBS := diff --git a/docs/CONTRACT_BUDGET.md b/docs/CONTRACT_BUDGET.md index a27c07ba45..23249558c6 100644 --- a/docs/CONTRACT_BUDGET.md +++ b/docs/CONTRACT_BUDGET.md @@ -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` diff --git a/docs/api-reference/generated/overview.md b/docs/api-reference/generated/overview.md index ffde5f42cc..85cf0b0d12 100644 --- a/docs/api-reference/generated/overview.md +++ b/docs/api-reference/generated/overview.md @@ -8,26 +8,14 @@ -- Package: `flext_core` - Version: `0.12.0` - Description: Enterprise Foundation Framework - Modern Python 3.13 + Clean Architecture -- Doc summary: Flext Core package. -- Classifiers: Development Status :: 3 - Alpha, Framework :: Pydantic :: 2, Framework :: - Pydantic, Intended Audience :: Developers, Operating System :: OS Independent, - Programming Language :: Python :: 3 :: Only, Programming Language :: Python :: 3.13, - Topic :: Software Development :: Libraries :: Application Frameworks, Typing :: Typed -- Project class: `domain` -- Keywords: `clean-architecture`, `ddd`, `enterprise`, `flext`, `modern`, `solid` -- Main facades: `FlextApi`, `FlextBase`, `FlextCli`, `FlextConfig`, `FlextConstants`, - `FlextConstantsEnforcement`, `FlextContainer`, `FlextContext` (+18 more) -- Alias exports: `c`, `d`, `e`, `h`, `m`, `p`, `r`, `s`, `t`, `u`, `x` -- Public symbol exports: `FlextApi`, `FlextBase`, `FlextCli`, `FlextConfig`, - `FlextConstants`, `FlextConstantsEnforcement`, `FlextContainer`, `FlextContext`, - `FlextDecorators`, `FlextDispatcher` (+20 more) -- Exported module shortcuts: `services` -- Generated module pages: `22` +- Governed projects: `0` +- Project classes: _none_ + +Generated from workspace discovery, `pyproject.toml`, public exports, and docstrings. ## Next Pages -- [Public API](public-api.md) -- [Module Index](modules/index.md) +- [Workspace Module Pages](projects/index.md) +- [Project Catalog](../../projects/generated/catalog.md) diff --git a/docs/api-reference/generated/projects/index.md b/docs/api-reference/generated/projects/index.md new file mode 100644 index 0000000000..f455549003 --- /dev/null +++ b/docs/api-reference/generated/projects/index.md @@ -0,0 +1,13 @@ +# Workspace Module Pages + + + +- No sections found + + + + + +Each project renders one page per public module, driven by docstrings. + +_No projects discovered._ diff --git a/docs/architecture/clean-architecture.md b/docs/architecture/clean-architecture.md index 80668fd7aa..5e2798468c 100644 --- a/docs/architecture/clean-architecture.md +++ b/docs/architecture/clean-architecture.md @@ -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) │ @@ -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** diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index d331e028ed..a4adf0a6a0 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -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) │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ @@ -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, @@ -80,7 +80,7 @@ Canonical references: `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). + (the core runtime registry: one validated write path, singleton plus scopes). - **L3 – application orchestration** diff --git a/docs/development/contributing.md b/docs/development/contributing.md index 31feee44db..deb50f62d9 100644 --- a/docs/development/contributing.md +++ b/docs/development/contributing.md @@ -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 diff --git a/docs/guides/dependency-injection-advanced.md b/docs/guides/dependency-injection-advanced.md index cb8a61ec53..40cfc5e1f5 100644 --- a/docs/guides/dependency-injection-advanced.md +++ b/docs/guides/dependency-injection-advanced.md @@ -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) @@ -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 @@ -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. diff --git a/docs/guides/service-patterns.md b/docs/guides/service-patterns.md index 664d1e59f5..30044d063d 100644 --- a/docs/guides/service-patterns.md +++ b/docs/guides/service-patterns.md @@ -117,8 +117,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 @@ -133,7 +134,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 diff --git a/docs/projects/generated/catalog.md b/docs/projects/generated/catalog.md index a0fa80049f..4fe3eba19c 100644 --- a/docs/projects/generated/catalog.md +++ b/docs/projects/generated/catalog.md @@ -12,4 +12,4 @@ Project links resolve to the generated root API reference for each governed FLEX package. | project | class | package | description | -| ------- | ----- | ------- | ----------- | +|---|---|---|---| diff --git a/examples/ex_08_container_lifecycle.py b/examples/ex_08_container_lifecycle.py index a59440b68e..41fc95bbdb 100644 --- a/examples/ex_08_container_lifecycle.py +++ b/examples/ex_08_container_lifecycle.py @@ -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 @@ -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)}") diff --git a/examples/ex_08_container_registration.py b/examples/ex_08_container_registration.py index ef143d15ca..be57f2a6aa 100644 --- a/examples/ex_08_container_registration.py +++ b/examples/ex_08_container_registration.py @@ -2,8 +2,10 @@ from __future__ import annotations +from examples.constants import c from examples.protocols import p from examples.shared import ExamplesFlextShared +from flext_core import e from examples.typings import t from examples.utilities import u @@ -25,50 +27,36 @@ def _exercise_registration_and_resolution(self, container: p.Container) -> None: resource_name = f"svc.{self.rand_str(6)}" missing_name = f"svc.{self.rand_str(6)}" bad_factory_name = f"svc.{self.rand_str(6)}" - empty_name_value = self.rand_int(1, 1000) register_ok = container.bind(service_name, service_value) - service_before_dup = container.has(service_name) - register_dup = container.bind(service_name, self.rand_int(1, 1000)) - service_after_dup = container.has(service_name) - empty_before_register = container.has("") - register_empty = container.bind("", empty_name_value) - empty_after_register = container.has("") self.audit_check("register.service.returns_self", register_ok is container) self.audit_check( "register.service.success", container.resolve(service_name).success ) self.audit_check( "register.service.stored_value_matches", - ( - container.resolve(service_name, type_cls=int).value - if container.resolve(service_name, type_cls=int).success - else -1 + container.resolve(service_name, type_cls=int).unwrap() == service_value, + ) + try: + _ = container.bind(service_name, self.rand_int(1, 1000)) + except e.ValidationError as exc: + self.audit_check("register.service.duplicate_raises", service_name in str(exc)) + self.audit_check( + "register.service.duplicate_keeps_first", + container.resolve(service_name, type_cls=int).unwrap() == service_value, + ) + try: + _ = container.bind("", self.rand_int(1, 1000)) + except e.ValidationError as exc: + self.audit_check( + "register.service.empty_name_raises", + str(exc).endswith(c.ERR_CONTAINER_NAME_EMPTY), + ) + try: + _ = container.bind(c.ServiceName.LOGGER, self.rand_int(1, 1000)) + except e.ValidationError as exc: + self.audit_check( + "register.service.reserved_name_raises", c.ServiceName.LOGGER in str(exc) ) - == service_value, - ) - self.audit_check( - "register.service.duplicate_returns_self", register_dup is container - ) - self.audit_check( - "register.service.duplicate_failure", - service_before_dup - and service_after_dup - and ( - ( - container.resolve(service_name, type_cls=int).value - if container.resolve(service_name, type_cls=int).success - else -1 - ) - == service_value - ), - ) - self.audit_check( - "register.service.empty_name_returns_self", register_empty is container - ) - self.audit_check( - "register.service.empty_name_failure", - not empty_before_register and (not empty_after_register), - ) factory_calls = {"count": 0} def _factory_counter() -> int: @@ -76,7 +64,10 @@ def _factory_counter() -> int: return factory_calls["count"] register_factory_ok = container.factory(factory_name, _factory_counter) - register_factory_dup = container.factory(factory_name, _factory_counter) + try: + _ = container.factory(factory_name, _factory_counter) + except e.ValidationError as exc: + self.audit_check("register.factory.duplicate_raises", factory_name in str(exc)) def _factory_raises() -> int: error_message = self.rand_str(10) @@ -87,17 +78,8 @@ def _factory_raises() -> int: "register.factory.returns_self", register_factory_ok is container ) self.audit_check( - "register.factory.success", container.resolve(factory_name).success - ) - self.audit_check( - "register.factory.duplicate_failure", - register_factory_dup is container - and container.resolve(factory_name).success, - ) - self.audit_check( - "register.factory.bad_registration_success", - register_factory_bad is container - and container.resolve(bad_factory_name).success, + "register.factory.raising_factory_registers", + register_factory_bad is container and container.has(bad_factory_name), ) resource_calls = {"count": 0} @@ -106,45 +88,28 @@ def _resource_data() -> t.IntMapping: return {self.rand_str(4): resource_calls["count"]} register_resource_ok = container.resource(resource_name, _resource_data) - register_resource_dup = container.resource(resource_name, _resource_data) + try: + _ = container.resource(service_name, _resource_data) + except e.ValidationError as exc: + self.audit_check( + "register.resource.cross_kind_duplicate_raises", service_name in str(exc) + ) self.audit_check( "register.resource.returns_self", register_resource_ok is container ) - self.audit_check( - "register.resource.success", container.resolve(resource_name).success - ) - self.audit_check( - "register.resource.duplicate_failure", - register_resource_dup is container - and container.resolve(resource_name).success, - ) get_service = container.resolve(service_name) get_factory = container.resolve(factory_name) get_resource = container.resolve(resource_name) get_missing = container.resolve(missing_name) get_bad_factory = container.resolve(bad_factory_name) self.audit_check("get.service.success", get_service.success) - self.audit_check( - "get.service.value_matches", - ( - container.resolve(service_name, type_cls=int).value - if container.resolve(service_name, type_cls=int).success - else -1 - ) - == service_value, - ) + self.audit_check("get.service.value_matches", get_service.unwrap() == service_value) self.audit_check("get.factory.success", get_factory.success) self.audit_check( - "get.factory.value_first_call", - ( - container.resolve(factory_name, type_cls=int).value - if container.resolve(factory_name, type_cls=int).success - else -1 - ) - == 1, + "get.factory.invoked_per_resolve", factory_calls["count"] ) self.audit_check("get.resource.success", get_resource.success) - self.audit_check("get.resource.call_count_is_one", resource_calls["count"] == 1) + self.audit_check("get.resource.invoked_per_resolve", resource_calls["count"]) self.audit_check("get.missing.failure", get_missing.failure) self.audit_check("get.bad_factory.failure", get_bad_factory.failure) get_typed_service = container.resolve(service_name, type_cls=int) @@ -169,7 +134,7 @@ def _resource_data() -> t.IntMapping: self.audit_check("has_service.service.true", container.has(service_name)) self.audit_check("has_service.factory.true", container.has(factory_name)) self.audit_check("has_service.resource.true", container.has(resource_name)) - self.audit_check("has_service.missing.false", container.has(missing_name)) + self.audit_check("has_service.missing.false", not container.has(missing_name)) service_list = list(container.names()) self.audit_check("list_services.contains.service", service_name in service_list) self.audit_check("list_services.contains.factory", factory_name in service_list) diff --git a/examples/ex_08_container_scoped.py b/examples/ex_08_container_scoped.py index 893d48f7b4..6747ebfd04 100644 --- a/examples/ex_08_container_scoped.py +++ b/examples/ex_08_container_scoped.py @@ -1,37 +1,21 @@ -"""Container scoped/wiring example section.""" +"""Container scope example section.""" from __future__ import annotations -import sys -from typing import TYPE_CHECKING - from examples.models import m from examples.protocols import p -from flext_core import FlextSettings, u +from flext_core import FlextSettings from .ex_08_container_registration import Ex08ContainerRegistration -if TYPE_CHECKING: - from types import ModuleType - - -class _WireProbe: - """Probe class used to exercise wire_modules(classes=...).""" - - class Ex08ContainerScoped(Ex08ContainerRegistration): """Scoped container checks for the container example.""" - def _exercise_wiring_and_scoped( + def _exercise_scoped( self, container: p.ContainerLifecycle ) -> p.ContainerLifecycle: - """Exercise wire_modules and scoped with all supported parameter styles.""" - self.section("wiring_and_scoped") - this_module: ModuleType = sys.modules[__name__] - container.wire(modules=[this_module]) - container.wire(packages=[]) - container.wire(classes=[_WireProbe]) - self.audit_check("wire_modules.calls_completed", True) + """Exercise scope with all supported parameter styles.""" + self.section("scoped") scoped_default = container.scope() subproject_alpha = self.rand_str(6) subproject_beta = self.rand_str(6) @@ -48,19 +32,24 @@ def _exercise_wiring_and_scoped( scoped_resource_value = self.rand_str(8) scoped_full = container.scope( subproject=subproject_beta, - registration=u.normalize_service_registration_spec( - m.ServiceRegistrationSpec( - settings=explicit_settings, - context=explicit_context, - services={scoped_service_name: scoped_service_value}, - factories={scoped_factory_name: lambda: scoped_factory_value}, - resources={ - scoped_resource_name: lambda: {"res": scoped_resource_value} - }, - ) + registration=m.ServiceRegistrationSpec( + settings=explicit_settings, + context=explicit_context, + services={scoped_service_name: scoped_service_value}, + factories={scoped_factory_name: lambda: scoped_factory_value}, + resources={scoped_resource_name: lambda: {"res": scoped_resource_value}}, ), ) self.audit_check("scoped.default.new_instance", scoped_default is not container) + self.audit_check( + "scoped.default.logger_stays_internal", + not scoped_default.has("logger") + and scoped_default.resolve("logger").success, + ) + self.audit_check( + "scoped.default.settings_service_is_own", + scoped_default.resolve("settings").unwrap() is scoped_default.settings, + ) self.audit_check( "scoped.default.inherits_service", scoped_default.has(self._registered_service_name), diff --git a/examples/ex_08_flext_container.expected b/examples/ex_08_flext_container.expected index 6dac45f2c3..0035ebbac4 100644 --- a/examples/ex_08_flext_container.expected +++ b/examples/ex_08_flext_container.expected @@ -13,23 +13,21 @@ constants.default_max_services: 1000 register.service.returns_self: True register.service.success: True register.service.stored_value_matches: True -register.service.duplicate_returns_self: True -register.service.duplicate_failure: True -register.service.empty_name_returns_self: True -register.service.empty_name_failure: True +register.service.duplicate_raises: True +register.service.duplicate_keeps_first: True +register.service.empty_name_raises: True +register.service.reserved_name_raises: True +register.factory.duplicate_raises: True register.factory.returns_self: True -register.factory.success: True -register.factory.duplicate_failure: True -register.factory.bad_registration_success: False +register.factory.raising_factory_registers: True +register.resource.cross_kind_duplicate_raises: True register.resource.returns_self: True -register.resource.success: True -register.resource.duplicate_failure: True get.service.success: True get.service.value_matches: True get.factory.success: True -get.factory.value_first_call: False +get.factory.invoked_per_resolve: 1 get.resource.success: True -get.resource.call_count_is_one: False +get.resource.invoked_per_resolve: 1 get.missing.failure: True get.bad_factory.failure: True get_typed.service.success: True @@ -41,7 +39,7 @@ get_typed.missing.failure: True has_service.service.true: True has_service.factory.true: True has_service.resource.true: True -has_service.missing.false: False +has_service.missing.false: True list_services.contains.service: True list_services.contains.factory: True list_services.contains.resource: True @@ -57,14 +55,15 @@ with_service.get.value_matches: True with_factory.get.value_matches: True with_resource.get.value_matches: True -[wiring_and_scoped] -wire_modules.calls_completed: True +[scoped] scoped.default.new_instance: True +scoped.default.logger_stays_internal: True +scoped.default.settings_service_is_own: True scoped.default.inherits_service: True scoped.default.get_typed_service_matches: True scoped.subproject.context_marker: True scoped.full.new_instance: True -scoped.full.settings_timezone: 'scoped/egyayiyc' +scoped.full.settings_timezone: 'scoped/lvfsdhvi' scoped.full.uses_explicit_context: True scoped.full.has_service: True scoped.full.has_factory: True @@ -74,12 +73,10 @@ scoped.full.get_factory_matches: True scoped.full.get_resource.success: True [internal_and_cleanup] -initialize_di_components.bridge_exists: True -initialize_di_components.container_exists: True initialize_registrations.list_services_empty: 0 -sync_settings_to_di.service_settings_present: False -register_core_services.logger_present: False -register_core_services.command_bus_present: False +core_services.settings_internal: True +core_services.logger_internal: True +core_services.command_bus_internal: True create_module_logger.explicit.type: 'FlextUtilitiesLogging' create_module_logger.explicit_custom.type: 'FlextUtilitiesLogging' unregister.existing.success: True diff --git a/examples/ex_08_flext_container.py b/examples/ex_08_flext_container.py index 23b068a4a8..0ea0c2e236 100644 --- a/examples/ex_08_flext_container.py +++ b/examples/ex_08_flext_container.py @@ -22,7 +22,7 @@ def exercise(self) -> None: root = self._exercise_singleton_and_creation() self._exercise_registration_and_resolution(root) self._exercise_fluent_and_settings(root) - scoped_full = self._exercise_wiring_and_scoped(root) + scoped_full = self._exercise_scoped(root) self._exercise_internal_and_cleanup(scoped_full, root) def _exercise_fluent_and_settings(self, container: p.Container) -> None: diff --git a/examples/ex_10_flext_handlers.expected b/examples/ex_10_flext_handlers.expected deleted file mode 100644 index 2dad595121..0000000000 --- a/examples/ex_10_flext_handlers.expected +++ /dev/null @@ -1,116 +0,0 @@ -[handler_core] -handle.not_implemented_pattern: 'NotImplementedError:' -handler.handler_name: '_DemoHandler' -handler.name_matches: True -handler.mode: 'command' -validate.none.failure: True -validate.ok.success: True -validate.blocked_cmd: 'blocked' -validate.blocked_qry: 'blocked' -validate.consistent: True -can_handle.expected: True -can_handle.derived: True -can_handle.other: False -execute.success.value: True -execute.validation_failure: 'Failed to execute handler validation: blocked' -dispatch.success: True -dispatch.mode_mismatch: "Failed to validate handler pipeline mode: Handler with mode 'command' cannot execute query pipelines" -dispatch.pipeline_exception: 'Failed to run handler pipeline: Critical handler failure: forced boom' -record_metric.ok: True -push_context.mapping: True -push_context.execution: True -pop_context.1: 'tefwel' -pop_context.2: 'dvxuqt' - -[create_from_callable] -callable.default: 'default:bgseao' -callable.default.matches: True -callable.named.handler_name: 'xmwyxmolek' -callable.named.name_matches: True -callable.named.mode: 'query' -callable.named.value_matches: True -callable.mode_enum: 'event' -callable.mode_enum.value_matches: True -callable.mode_str: 'query' -callable.mode_str.value_matches: True -callable.handler_settings.name: 'ishnpeysnl' -callable.handler_settings.mode: 'saga' -callable.handler_settings.name_matches: True -callable.handler_settings.value_matches: True -callable.invalid_mode: 'ValidationError:[VALIDATION_ERROR] Invalid handler mode: invalid' - -[discovery] -scan_class.count: 2 -scan_class.first: 'high' -has_handlers.class: True -has_handlers.class_none: False -scan_module.count: 1 -scan_module.name: 'mod_handler' -scan_module.wrapped_result: True -has_handlers.module.true: False -has_handlers.module.false: True - -[namespaces_and_mixins] -bootstrap.create_instance: '_NoArgs' -cqrs.record_metric: True -cqrs.metrics: 5 -cqrs.push_context.mapping: True -cqrs.current_context: 'otfapl' -cqrs.pop_context: 'otfapl' -di.bind_configuration_exists: True -di.register_object: True -di.register_factory.cached: True -di.register_resource: True -di.duplicate_error: "ValueError:Provider 'ixnbm' is already registered" -di.layered.bridge: 'DynamicContainer' -di.layered.services: 'DynamicContainer' -di.layered.resources: 'DynamicContainer' -di.wire.noop: True -integration.calls: True -metadata.version: '7.7.7' -metadata.attributes: dict -protocol.handler.true: True -protocol.handler.false: True - -[runtime_result_and_utilities] -rr.is_success: True -rr.is_failure: True -rr.unwrap_or: True -rr.map: True -rr.flat_map: True -rr.and_then: True -rr.alt: True -rr.lash: True -rr.recover: True -rr.fold: True -mixin.ok: True -mixin.fail: True -mixin.ensure_result.value: True -mixin.ensure_result.result: True -mixin.to_dict: ConfigMap -runtime.generate_id.length: True -runtime.generate_id.unique: True -runtime.generate_prefixed_id.default: True -runtime.generate_prefixed_id.length: True -runtime.generate_datetime_utc: True -runtime.compare_entities.true: True -runtime.compare_entities.false: False -runtime.hash_entity: True -runtime.compare_value_objects.scalar: True -runtime.compare_value_objects.model: False -runtime.hash_value_object: True -runtime.ensure_trace_context.source: 'rzdoi' -runtime.ensure_trace_context.keys: list -runtime.resolve_log_level: True -runtime.is_dict_like.true: True -runtime.is_dict_like.false: False -runtime.list_like.true: True -runtime.list_like.false: False -runtime.is_valid_identifier.true: True -runtime.is_valid_identifier.false: False -runtime.safe_get_attribute: True -runtime.extract_generic_args: True -runtime.is_sequence_type.true: True -runtime.is_sequence_type.false: False -runtime.normalize_general: dict -runtime.normalize_metadata: dict diff --git a/mkdocs.yml b/mkdocs.yml index 2212321245..3b94f76f40 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,10 +1,9 @@ # AUTO-GENERATED — DO NOT EDIT MANUALLY -site_name: flext-core Documentation -site_description: Generated documentation for flext-core +site_name: flext-core +site_description: Workspace documentation generated from code and curated root docs site_url: https://github.com/flext-sh/flext-core/blob/main/README.md repo_name: flext-sh/flext-core repo_url: https://github.com/flext-sh/flext-core -edit_uri: edit/main/flext-core/ docs_dir: docs site_dir: .reports/docs/site @@ -88,7 +87,9 @@ markdown_extensions: custom_checkbox: true # MkDocs discovers the complete documentation tree, including maintained pages. -# A fixed three-page nav silently omitted every domain guide and decision record. +# A fixed three-page nav silently omitted guides, standards, and decisions. +not_in_nav: | + api-reference/generated/** validation: omitted_files: warn absolute_links: warn diff --git a/pyproject.toml b/pyproject.toml index 082adc7538..98c968f0ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,8 @@ +# @flext-generated: continuous +# @flext-owner: flext-infra/config/codegen.yaml + flext-infra/src/flext_infra/templates/project/base/pyproject.toml.j2 +# @flext-adjust: MANAGED=conflict_sections + overwrite_project_keys. CUSTOM=preserve_project_keys and [tool.*] outside conflict_sections. Never edit this projection. +# @flext-regenerate: make gen + [build-system] build-backend = "hatchling.build" requires = ["hatchling"] @@ -130,7 +135,6 @@ exclude_also = [ "if TYPE_CHECKING:", "if typing.TYPE_CHECKING:", ] -fail_under = 45 precision = 2 show_missing = true skip_covered = false diff --git a/src/flext_core/_constants/_errors_parts/flextconstantserrors_part_05.py b/src/flext_core/_constants/_errors_parts/flextconstantserrors_part_05.py index 3572eae5f7..a310fb78f3 100644 --- a/src/flext_core/_constants/_errors_parts/flextconstantserrors_part_05.py +++ b/src/flext_core/_constants/_errors_parts/flextconstantserrors_part_05.py @@ -13,38 +13,20 @@ class FlextConstantsErrorsRuntimeSettings: """Container, runtime, exceptions, lazy, and settings errors.""" # --- Container / Runtime --- - ERR_CONTAINER_FACTORY_INVALID_REGISTERABLE: Final[str] = ( - "Factory '{name}' returned value that does not satisfy RegisterableService" - " protocol. Expected a canonical registerable service, protocol, or callable." - ) ERR_CONTAINER_REGISTRATION_FAILED: Final[str] = ( "Container registration of '{name}' failed: {reason}" ) - ERR_CONTAINER_CONFIG_NOT_INITIALIZED: Final[str] = ( - "Configuration must be initialized via initialize_registrations" - ) - ERR_CONTAINER_CONTEXT_NOT_INITIALIZED: Final[str] = ( - "Context not initialized. Provide context during container creation via " - "FlextContainer(registration=m.ServiceRegistrationSpec(context=...)) or " - "FlextContainer.shared(context=...)" - ) - ERR_CONTAINER_PROVIDE_HELPER_NOT_INITIALIZED: Final[str] = ( - "DI bridge Provide helper not initialized" - ) - ERR_CONTAINER_PROVIDE_HELPER_UNSUPPORTED_TYPE: Final[str] = ( - "DI bridge Provide helper returned unsupported type" - ) - ERR_CONTAINER_BRIDGE_MUST_HAVE_CONFIG_PROVIDER: Final[str] = ( - "Bridge must have settings provider" + ERR_CONTAINER_NAME_EMPTY: Final[str] = ( + "Container registration requires a non-empty name" ) - ERR_CONTAINER_BRIDGE_CONFIG_PROVIDER_CANNOT_BE_NONE: Final[str] = ( - "Bridge settings provider cannot be None" + ERR_CONTAINER_NAME_DUPLICATE: Final[str] = ( + "Container name '{name}' is already registered; drop it before rebinding" ) - ERR_CONTAINER_BRIDGE_CONFIG_PROVIDER_MUST_SUPPORT_OVERRIDE: Final[str] = ( - "Bridge settings provider must support override()" + ERR_CONTAINER_NAME_RESERVED: Final[str] = ( + "Container name '{name}' is reserved for the core runtime services" ) - ERR_RUNTIME_PROVIDER_ALREADY_REGISTERED: Final[str] = ( - "Provider '{name}' is already registered" + ERR_CONTAINER_CALLER_UNRESOLVED: Final[str] = ( + "auto_register_factories requires a caller module imported in sys.modules" ) ERR_RUNTIME_METADATA_MODEL_NOT_BOUND: Final[str] = ( "FlextRuntime.Metadata is not bound to a concrete model" @@ -103,7 +85,6 @@ class FlextConstantsErrorsRuntimeSettings: ERR_SETTINGS_NAMESPACE_NOT_REGISTERED: Final[str] = ( "Namespace '{namespace}' not registered" ) - ERR_SETTINGS_DI_PROVIDER_NOT_INITIALIZED: Final[str] = "DI provider not initialized" ERR_SETTINGS_CLASS_REQUIRED_FOR_NON_DECORATOR: Final[str] = ( "settings_class is required when decorator=False" ) diff --git a/src/flext_core/_constants/infrastructure.py b/src/flext_core/_constants/infrastructure.py index b127492462..6545a1ee2c 100644 --- a/src/flext_core/_constants/infrastructure.py +++ b/src/flext_core/_constants/infrastructure.py @@ -9,6 +9,8 @@ from enum import StrEnum, unique from typing import ClassVar +from .file import FlextConstantsFile +from .mixins import FlextConstantsMixins from .timeout import FlextConstantsTimeout @@ -79,6 +81,14 @@ class ServiceName(StrEnum): LOGGER = "logger" COMMAND_BUS = "command_bus" + CONTAINER_RESERVED_NAMES: ClassVar[frozenset[str]] = frozenset({ + FlextConstantsFile.Directory.CONFIG, + ServiceName.LOGGER, + FlextConstantsMixins.FIELD_CONTEXT, + ServiceName.COMMAND_BUS, + }) + """Names the container writes for the core runtime; public writes reject them.""" + @unique class HandlerMode(StrEnum): """Dispatcher handler processing modes.""" diff --git a/src/flext_core/_models/_container_parts/flextmodelscontainer_part_01.py b/src/flext_core/_models/_container_parts/flextmodelscontainer_part_01.py index 9b84c3b137..733121a9f8 100644 --- a/src/flext_core/_models/_container_parts/flextmodelscontainer_part_01.py +++ b/src/flext_core/_models/_container_parts/flextmodelscontainer_part_01.py @@ -15,10 +15,9 @@ from datetime import datetime from typing import Annotated -from flext_core import c, t +from flext_core import t from ..._runtime._container import FlextRuntimeContainer as FlextRuntime -from ..._typings.pydantic import FlextTypesPydantic as tp from ..._utilities.generators import FlextUtilitiesGenerators as ug from ..._utilities.pydantic import FlextUtilitiesPydantic as up from ..base import FlextModelsBase as m @@ -41,7 +40,6 @@ class ServiceRegistration(m.ArbitraryTypesModel): ] service: Annotated[ t.RegisterableService, - tp.SkipValidation, mp.Field( ..., description="Service instance (protocols, models, callables)" ), @@ -94,7 +92,6 @@ class FactoryRegistration(m.ArbitraryTypesModel): ] factory: Annotated[ t.FactoryCallable, - tp.SkipValidation, mp.Field( ..., description="Factory function that creates service instances" ), @@ -105,17 +102,6 @@ class FactoryRegistration(m.ArbitraryTypesModel): description="Timestamp when factory was registered (configured timezone)" ), ] = mp.Field(default_factory=ug.now) - is_singleton: Annotated[ - bool, - mp.Field(False, description="Whether factory creates singleton instances"), - ] = False - cached_instance: Annotated[ - t.RegisterableService | None, - tp.SkipValidation, - mp.Field( - None, description="Cached singleton instance (if is_singleton=True)" - ), - ] = None metadata: Annotated[ m.Metadata | FlextModelsContainers.ConfigMap | None, mp.BeforeValidator( @@ -127,13 +113,6 @@ class FactoryRegistration(m.ArbitraryTypesModel): None, description="Additional factory metadata (JSON-serializable)" ), ] = None - invocation_count: Annotated[ - t.NonNegativeInt, - mp.Field( - c.DEFAULT_MAX_COMMAND_RETRIES, - description="Number of times factory has been invoked", - ), - ] = c.DEFAULT_MAX_COMMAND_RETRIES __all__: list[str] = ["FlextModelsContainer"] diff --git a/src/flext_core/_models/_container_parts/flextmodelscontainer_part_02.py b/src/flext_core/_models/_container_parts/flextmodelscontainer_part_02.py index 199977522c..5722a940dd 100644 --- a/src/flext_core/_models/_container_parts/flextmodelscontainer_part_02.py +++ b/src/flext_core/_models/_container_parts/flextmodelscontainer_part_02.py @@ -18,7 +18,6 @@ from flext_core import c, t from ..._runtime._container import FlextRuntimeContainer as FlextRuntime -from ..._typings.pydantic import FlextTypesPydantic as tp from ..._utilities.generators import FlextUtilitiesGenerators as ug from ..base import FlextModelsBase as m from ..containers import FlextModelsContainers @@ -32,8 +31,8 @@ class FlextModelsContainer(FlextModelsContainerPart01): class ResourceRegistration(m.ArbitraryTypesModel): """Model for lifecycle-managed resource registrations. - Captures resource factories that dependency-injector should wrap via - ``providers.Resource`` for connection-style dependencies (DB/HTTP). + Captures resource factories invoked on every resolve for + connection-style dependencies (DB/HTTP). """ name: Annotated[ @@ -41,7 +40,6 @@ class ResourceRegistration(m.ArbitraryTypesModel): ] factory: Annotated[ t.ResourceCallable, - tp.SkipValidation, mp.Field( ..., description="Factory returning the lifecycle-managed resource" ), diff --git a/src/flext_core/_models/_container_parts/flextmodelscontainer_part_03.py b/src/flext_core/_models/_container_parts/flextmodelscontainer_part_03.py index d3abaac809..230d9f77cf 100644 --- a/src/flext_core/_models/_container_parts/flextmodelscontainer_part_03.py +++ b/src/flext_core/_models/_container_parts/flextmodelscontainer_part_03.py @@ -16,7 +16,6 @@ from flext_core import p, t -from ..._typings.pydantic import FlextTypesPydantic as tp from ..base import FlextModelsBase as m from ..containers import FlextModelsContainers from ..pydantic import FlextModelsPydantic as mp @@ -39,7 +38,6 @@ class ServiceRegistrationSpec(m.ArbitraryTypesModel): settings: Annotated[ p.Settings | None, - tp.SkipValidation, mp.Field( None, title="Config", @@ -48,7 +46,6 @@ class ServiceRegistrationSpec(m.ArbitraryTypesModel): ] = None context: Annotated[ p.Context | None, - tp.SkipValidation, mp.Field( None, title="Context", @@ -60,7 +57,6 @@ class ServiceRegistrationSpec(m.ArbitraryTypesModel): str, FlextModelsContainer.ServiceRegistration | t.RegisterableService ] | None, - tp.SkipValidation, mp.Field( None, title="Services", @@ -73,7 +69,6 @@ class ServiceRegistrationSpec(m.ArbitraryTypesModel): str, FlextModelsContainer.FactoryRegistration | t.FactoryCallable ] | None, - tp.SkipValidation, mp.Field( None, title="Factories", @@ -86,7 +81,6 @@ class ServiceRegistrationSpec(m.ArbitraryTypesModel): str, FlextModelsContainer.ResourceRegistration | t.ResourceCallable ] | None, - tp.SkipValidation, mp.Field( None, title="Resources", diff --git a/src/flext_core/_protocols/_container_parts/flextprotocolscontainer_part_01.py b/src/flext_core/_protocols/_container_parts/flextprotocolscontainer_part_01.py index 068fcbdd91..39dfd24cd0 100644 --- a/src/flext_core/_protocols/_container_parts/flextprotocolscontainer_part_01.py +++ b/src/flext_core/_protocols/_container_parts/flextprotocolscontainer_part_01.py @@ -8,11 +8,8 @@ from typing import TYPE_CHECKING, Protocol, runtime_checkable -from ..base import FlextProtocolsBase if TYPE_CHECKING: - from types import ModuleType - from flext_core import t @@ -37,56 +34,5 @@ class MutableRootDict[RootValueT](Protocol): @property def root(self) -> t.MutableMappingKV[str, RootValueT]: ... - @runtime_checkable - class ProviderLike[T_co](Protocol): - """DI-free abstraction for dependency injection providers. - - Provides a framework-independent contract for dependency injection - providers. Real providers in ``FlextRuntime.DependencyIntegration`` - implement this Protocol structurally. - - Usage:: - - provider: p.ProviderLike[MyService] - service = provider() # Returns MyService instance - - This Protocol avoids coupling the ``_protocols`` layer to - ``dependency_injector``, keeping the architecture boundary clean. - """ - - def __call__(self) -> T_co: - """Resolve and return the provided dependency.""" - ... - - @runtime_checkable - class ContainerCreationOptions(FlextProtocolsBase.Base, Protocol): - """Structural contract for DI container bootstrap options.""" - - @property - def settings( - self, - ) -> FlextProtocolsContainer.RootDict[t.JsonPayload] | None: ... - - @property - def services(self) -> t.MappingKV[str, t.RegisterableService] | None: ... - - @property - def factories(self) -> t.MappingKV[str, t.FactoryCallable] | None: ... - - @property - def resources(self) -> t.MappingKV[str, t.ResourceCallable] | None: ... - - @property - def wire_modules(self) -> t.SequenceOf[ModuleType] | None: ... - - @property - def wire_packages(self) -> t.StrSequence | None: ... - - @property - def wire_classes(self) -> t.SequenceOf[type] | None: ... - - @property - def factory_cache(self) -> bool: ... - __all__: list[str] = ["FlextProtocolsContainer"] diff --git a/src/flext_core/_protocols/_container_parts/flextprotocolscontainer_part_02.py b/src/flext_core/_protocols/_container_parts/flextprotocolscontainer_part_02.py index ba26ebf5dd..00bf29010f 100644 --- a/src/flext_core/_protocols/_container_parts/flextprotocolscontainer_part_02.py +++ b/src/flext_core/_protocols/_container_parts/flextprotocolscontainer_part_02.py @@ -12,9 +12,6 @@ from ..settings import FlextProtocolsSettings if TYPE_CHECKING: - from collections.abc import Callable - from types import ModuleType - from flext_core import m, t from ..context import FlextProtocolsContext @@ -47,11 +44,6 @@ def context(self) -> FlextProtocolsContext.Context: """Execution context bound to the container.""" ... - @property - def provide(self) -> Callable[[str], t.RegisterableService]: - """The dependency-injector Provide helper scoped to the bridge.""" - ... - def clear(self) -> None: """Clear all services and factories.""" ... @@ -129,15 +121,5 @@ def scope( """Create an isolated container scope with optional overrides.""" ... - def wire( - self, - *, - modules: t.SequenceOf[ModuleType] | None = None, - packages: t.StrSequence | None = None, - classes: t.SequenceOf[type] | None = None, - ) -> None: - """Wire modules/packages to the DI bridge for @inject/Provide usage.""" - ... - __all__: list[str] = ["FlextProtocolsContainer"] diff --git a/src/flext_core/_protocols/_container_parts/flextprotocolscontainer_part_03.py b/src/flext_core/_protocols/_container_parts/flextprotocolscontainer_part_03.py index dacfbe411a..286396927d 100644 --- a/src/flext_core/_protocols/_container_parts/flextprotocolscontainer_part_03.py +++ b/src/flext_core/_protocols/_container_parts/flextprotocolscontainer_part_03.py @@ -23,10 +23,6 @@ class FlextProtocolsContainer(FlextProtocolsContainerPart02): class ContainerLifecycle(FlextProtocolsContainerPart02.Container, Protocol): """Extended container contract for bootstrap and lifecycle operations.""" - def initialize_di_components(self) -> None: - """Initialize DI bridge and backing containers.""" - ... - def initialize_registrations( self, *, registration: m.ServiceRegistrationSpec | None = None ) -> None: @@ -38,14 +34,6 @@ def register_core_services(self) -> None: """Register the canonical core service set into the container.""" ... - def register_existing_providers(self) -> None: - """Hydrate dependency providers from current registrations.""" - ... - - def sync_config_to_di(self) -> None: - """Synchronize validated configuration into DI providers.""" - ... - @runtime_checkable class ContainerType[ TContainer: FlextProtocolsContainerPart02.Container = FlextProtocolsContainerPart02.Container diff --git a/src/flext_core/_runtime/__init__.py b/src/flext_core/_runtime/__init__.py index 1077e79f4b..5298a79051 100644 --- a/src/flext_core/_runtime/__init__.py +++ b/src/flext_core/_runtime/__init__.py @@ -11,10 +11,6 @@ if TYPE_CHECKING: from ._base import FlextRuntimeBase from ._container import FlextRuntimeContainer - from ._dependency import FlextRuntimeDependencyIntegration - from ._dependency_bindings import FlextRuntimeDependencyBindings - from ._dependency_options import FlextRuntimeDependencyOptions - from ._dependency_types import FlextRuntimeDependencyTypes from ._metadata import FlextRuntimeMetadata from ._metadata_validation import FlextRuntimeMetadataValidation @@ -22,10 +18,6 @@ __all__: tuple[str, ...] = ( "FlextRuntimeBase", "FlextRuntimeContainer", - "FlextRuntimeDependencyBindings", - "FlextRuntimeDependencyIntegration", - "FlextRuntimeDependencyOptions", - "FlextRuntimeDependencyTypes", "FlextRuntimeMetadata", "FlextRuntimeMetadataValidation", ) @@ -35,10 +27,6 @@ MappingProxyType({ "._base": ("FlextRuntimeBase",), "._container": ("FlextRuntimeContainer",), - "._dependency": ("FlextRuntimeDependencyIntegration",), - "._dependency_bindings": ("FlextRuntimeDependencyBindings",), - "._dependency_options": ("FlextRuntimeDependencyOptions",), - "._dependency_types": ("FlextRuntimeDependencyTypes",), "._metadata": ("FlextRuntimeMetadata",), "._metadata_validation": ("FlextRuntimeMetadataValidation",), }), diff --git a/src/flext_core/_runtime/_dependency.py b/src/flext_core/_runtime/_dependency.py deleted file mode 100644 index 9e22077946..0000000000 --- a/src/flext_core/_runtime/_dependency.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Composed dependency-injector runtime bridge. - -Copyright (c) 2025 FLEXT Team. All rights reserved. -SPDX-License-Identifier: MIT -""" - -from __future__ import annotations - -from ._dependency_bindings import FlextRuntimeDependencyBindings - - -class FlextRuntimeDependencyIntegration: - """Expose dependency-injector integration under the runtime namespace.""" - - class DependencyIntegration(FlextRuntimeDependencyBindings): - """Centralize dependency-injector wiring with provider helpers.""" - - -__all__: list[str] = ["FlextRuntimeDependencyIntegration"] diff --git a/src/flext_core/_runtime/_dependency_bindings.py b/src/flext_core/_runtime/_dependency_bindings.py deleted file mode 100644 index ac52710176..0000000000 --- a/src/flext_core/_runtime/_dependency_bindings.py +++ /dev/null @@ -1,182 +0,0 @@ -"""Dependency-injector runtime bridge bindings. - -Copyright (c) 2025 FLEXT Team. All rights reserved. -SPDX-License-Identifier: MIT -""" - -from __future__ import annotations - -import inspect -from typing import TYPE_CHECKING - -from dependency_injector import containers, providers, wiring -from dependency_injector.containers import Container - -from flext_core import c - -from ._dependency_options import FlextRuntimeDependencyOptions - -if TYPE_CHECKING: - from collections.abc import Callable, MutableSequence - from types import ModuleType - - from .._protocols.container import FlextProtocolsContainer as pc - from .._typings.base import FlextTypingBase as tb - from .._typings.services import FlextTypesServices as ts - - -class FlextRuntimeDependencyBindings(FlextRuntimeDependencyOptions): - """Register dependency-injector providers for runtime containers.""" - - @classmethod - def _populate_container( - cls, - di_container: containers.DynamicContainer, - opts: pc.ContainerCreationOptions, - ) -> None: - """Register settings, services, factories, resources, and wiring.""" - if opts.settings is not None: - _ = cls.bind_configuration(di_container, opts.settings) - if opts.services: - for name, instance in opts.services.items(): - _ = cls.register_object(di_container, name, instance) - if opts.factories: - for name, factory in opts.factories.items(): - _ = cls.register_factory( - di_container, name, factory, cache=opts.factory_cache - ) - if opts.resources: - for name, resource_factory in opts.resources.items(): - _ = cls.register_resource(di_container, name, resource_factory) - if opts.wire_modules or opts.wire_packages or opts.wire_classes: - cls.wire( - di_container, - modules=opts.wire_modules, - packages=opts.wire_packages, - classes=opts.wire_classes, - ) - - @classmethod - def create_container( - cls, - container_options: pc.ContainerCreationOptions - | tb.MappingKV[str, ts.JsonPayload] - | None = None, - **runtime_kwargs: ts.JsonPayload, - ) -> containers.DynamicContainer: - """Create a DynamicContainer with optional pre-registration and wiring.""" - base = cls._parse_options(container_options) - opts = cls._merge_options(base, runtime_kwargs) if runtime_kwargs else base - di_container = cls.DynamicContainerWithConfig() - cls._populate_container(di_container, opts) - return di_container - - @classmethod - def create_layered_bridge( - cls, settings: pc.RootDict[ts.JsonPayload] | None = None - ) -> tuple[ - containers.DeclarativeContainer, - containers.DynamicContainer, - containers.DynamicContainer, - ]: - """Create a DeclarativeContainer bridged to dynamic modules.""" - bridge = cls.BridgeContainer() - service_module = containers.DynamicContainer() - resource_module = containers.DynamicContainer() - bridge.services = providers.Object(service_module) - bridge.resources = providers.Object(resource_module) - cls.bind_configuration_provider(bridge.settings, settings) - return (bridge, service_module, resource_module) - - @classmethod - def bind_configuration( - cls, - di_container: containers.DynamicContainer, - settings: pc.RootDict[ts.JsonPayload] | None, - ) -> providers.Configuration: - """Bind configuration mapping to the DI container.""" - configuration_provider = providers.Configuration() - if settings: - configuration_provider.from_dict(dict(settings.root)) - if isinstance(di_container, cls.DynamicContainerWithConfig): - configured_container: FlextRuntimeDependencyBindings.DynamicContainerWithConfig = di_container - configured_container.settings = configuration_provider - else: - setattr(di_container, c.Directory.CONFIG, configuration_provider) - return configuration_provider - - @staticmethod - def bind_configuration_provider( - configuration_provider: providers.Configuration, - settings: pc.RootDict[ts.JsonPayload] | None, - ) -> providers.Configuration: - """Bind configuration directly to an existing provider.""" - if settings: - configuration_provider.from_dict(dict(settings.root)) - return configuration_provider - - @staticmethod - def register_factory[T]( - di_container: containers.DynamicContainer, - name: str, - factory: Callable[[], T], - *, - cache: bool = True, - ) -> providers.Provider[T]: - """Register a factory using Singleton/Factory providers.""" - if hasattr(di_container, name): - raise ValueError( - c.ERR_RUNTIME_PROVIDER_ALREADY_REGISTERED.format(name=name) - ) - provider: providers.Provider[T] = ( - providers.Singleton(factory) if cache else providers.Factory(factory) - ) - setattr(di_container, name, provider) - return provider - - @staticmethod - def register_object[T]( - di_container: containers.DynamicContainer, name: str, instance: T - ) -> providers.Provider[T]: - """Register a concrete instance using ``providers.Object``.""" - if hasattr(di_container, name): - raise ValueError( - c.ERR_RUNTIME_PROVIDER_ALREADY_REGISTERED.format(name=name) - ) - provider: providers.Provider[T] = providers.Object(instance) - setattr(di_container, name, provider) - return provider - - @staticmethod - def register_resource[T]( - di_container: containers.DynamicContainer, name: str, factory: Callable[[], T] - ) -> providers.Provider[T]: - """Register a resource provider for lifecycle-managed dependencies.""" - if hasattr(di_container, name): - raise ValueError( - c.ERR_RUNTIME_PROVIDER_ALREADY_REGISTERED.format(name=name) - ) - provider: providers.Provider[T] = providers.Resource(factory) - setattr(di_container, name, provider) - return provider - - @staticmethod - def wire( - container: Container, - *, - modules: tb.SequenceOf[ModuleType] | None = None, - packages: tb.StrSequence | None = None, - classes: tb.SequenceOf[type] | None = None, - ) -> None: - """Wire modules or packages to a dependency-injector container.""" - modules_to_wire: MutableSequence[ModuleType] = list(modules or []) - if classes: - for target_class in classes: - module = inspect.getmodule(target_class) - if module is not None: - modules_to_wire.append(module) - _ = packages - wiring.wire(container, modules=modules_to_wire or None, packages=None) - - -__all__: list[str] = ["FlextRuntimeDependencyBindings"] diff --git a/src/flext_core/_runtime/_dependency_options.py b/src/flext_core/_runtime/_dependency_options.py deleted file mode 100644 index 6bf522c8a4..0000000000 --- a/src/flext_core/_runtime/_dependency_options.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Dependency-injector runtime bridge option parsing. - -Copyright (c) 2025 FLEXT Team. All rights reserved. -SPDX-License-Identifier: MIT -""" - -from __future__ import annotations - -from collections.abc import Mapping, MutableMapping -from typing import TYPE_CHECKING - -from ._dependency_types import FlextRuntimeDependencyTypes - -if TYPE_CHECKING: - from .._protocols.container import FlextProtocolsContainer as pc - from .._typings.base import FlextTypingBase as tb - from .._typings.services import FlextTypesServices as ts - - -class FlextRuntimeDependencyOptions(FlextRuntimeDependencyTypes): - """Parse and merge dependency container creation options.""" - - @classmethod - def _parse_options( - cls, - container_options: pc.ContainerCreationOptions - | tb.MappingKV[str, ts.JsonPayload] - | None, - ) -> pc.ContainerCreationOptions: - """Parse raw container options into a validated model.""" - match container_options: - case None: - return cls.ContainerCreationOptions.model_validate({}) - case Mapping(): - return cls.ContainerCreationOptions.model_validate(container_options) - case _: - return cls.ContainerCreationOptions.model_validate( - { - field: getattr(container_options, field) - for field in cls._OPTION_FIELDS - } - | {"factory_cache": container_options.factory_cache} - ) - - @classmethod - def _merge_options( - cls, - base: pc.ContainerCreationOptions, - overrides: tb.MappingKV[str, ts.JsonPayload], - ) -> pc.ContainerCreationOptions: - """Merge runtime kwargs over base options.""" - override_opts = cls.ContainerCreationOptions.model_validate(overrides) - merged: MutableMapping[str, ts.JsonPayload] = { - field: ( - getattr(override_opts, field) - if getattr(override_opts, field) is not None - else getattr(base, field) - ) - for field in cls._OPTION_FIELDS - } - merged["factory_cache"] = override_opts.factory_cache - return cls.ContainerCreationOptions.model_validate(merged) - - -__all__: list[str] = ["FlextRuntimeDependencyOptions"] diff --git a/src/flext_core/_runtime/_dependency_types.py b/src/flext_core/_runtime/_dependency_types.py deleted file mode 100644 index 0530b358df..0000000000 --- a/src/flext_core/_runtime/_dependency_types.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Dependency-injector runtime bridge types. - -Copyright (c) 2025 FLEXT Team. All rights reserved. -SPDX-License-Identifier: MIT -""" - -from __future__ import annotations - -from types import ModuleType -from typing import Annotated, ClassVar - -from dependency_injector import containers, providers -from pydantic import BaseModel, ConfigDict - -from .._models.containers import FlextModelsContainers -from .._typings.base import FlextTypingBase as tb -from .._typings.pydantic import FlextTypesPydantic as tp -from .._typings.services import FlextTypesServices as ts - - -class FlextRuntimeDependencyTypes: - """Type owners for dependency-injector runtime bridge.""" - - class DynamicContainerWithConfig(containers.DynamicContainer): - """Dynamic container with declared configuration provider.""" - - settings: providers.Configuration = providers.Configuration() - - class BridgeContainer(containers.DeclarativeContainer): - """Declarative container grouping settings and resource modules.""" - - settings = providers.Configuration() - services = providers.Object(containers.DynamicContainer()) - resources = providers.Object(containers.DynamicContainer()) - - class ContainerCreationOptions(BaseModel): - """Validated options for dependency container creation.""" - - model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) - - settings: FlextModelsContainers.ConfigMap | None = None - services: ( - tb.MappingKV[str, Annotated[ts.RegisterableService, tp.SkipValidation]] - | None - ) = None - factories: ( - tb.MappingKV[str, Annotated[ts.FactoryCallable, tp.SkipValidation]] | None - ) = None - resources: ( - tb.MappingKV[str, Annotated[ts.ResourceCallable, tp.SkipValidation]] | None - ) = None - wire_modules: tb.SequenceOf[ModuleType] | None = None - wire_packages: tb.StrSequence | None = None - wire_classes: tb.SequenceOf[type] | None = None - factory_cache: bool = True - - _OPTION_FIELDS: ClassVar[tb.StrSequence] = ( - "settings", - "services", - "factories", - "resources", - "wire_modules", - "wire_packages", - "wire_classes", - ) - - -__all__: list[str] = ["FlextRuntimeDependencyTypes"] diff --git a/src/flext_core/_utilities/model_runtime.py b/src/flext_core/_utilities/model_runtime.py index 6458373568..534b5ec019 100644 --- a/src/flext_core/_utilities/model_runtime.py +++ b/src/flext_core/_utilities/model_runtime.py @@ -4,7 +4,7 @@ from importlib import import_module -from flext_core import m, p, t +from flext_core import m, p from .model_options import FlextUtilitiesModelOptions @@ -12,83 +12,6 @@ class FlextUtilitiesModelRuntime(FlextUtilitiesModelOptions): """Runtime DSL: dispatcher, registry, and service-runtime construction.""" - @staticmethod - def normalize_service_registrations( - registrations: t.MappingKV[str, m.ServiceRegistration | t.RegisterableService] - | None, - ) -> t.MappingKV[str, m.ServiceRegistration] | None: - """Normalize service values into service registration records.""" - return ( - None - if registrations is None - else { - name: ( - value - if isinstance(value, m.ServiceRegistration) - else m.ServiceRegistration( - name=name, service=value, service_type=value.__class__.__name__ - ) - ) - for name, value in registrations.items() - } - ) - - @staticmethod - def normalize_factory_registrations( - registrations: t.MappingKV[str, m.FactoryRegistration | t.FactoryCallable] - | None, - ) -> t.MappingKV[str, m.FactoryRegistration] | None: - """Normalize factory callables into factory registration records.""" - return ( - None - if registrations is None - else { - name: ( - value - if isinstance(value, m.FactoryRegistration) - else m.FactoryRegistration(name=name, factory=value) - ) - for name, value in registrations.items() - } - ) - - @staticmethod - def normalize_resource_registrations( - registrations: t.MappingKV[str, m.ResourceRegistration | t.ResourceCallable] - | None, - ) -> t.MappingKV[str, m.ResourceRegistration] | None: - """Normalize resource callables into resource registration records.""" - return ( - None - if registrations is None - else { - name: ( - value - if isinstance(value, m.ResourceRegistration) - else m.ResourceRegistration(name=name, factory=value) - ) - for name, value in registrations.items() - } - ) - - @classmethod - def normalize_service_registration_spec( - cls, registration: m.ServiceRegistrationSpec - ) -> m.ServiceRegistrationSpec: - """Normalize declarative bootstrap values into registration records.""" - normalized: m.ServiceRegistrationSpec = registration.model_copy( - update={ - "services": cls.normalize_service_registrations(registration.services), - "factories": cls.normalize_factory_registrations( - registration.factories - ), - "resources": cls.normalize_resource_registrations( - registration.resources - ), - } - ) - return normalized - @classmethod def build_dispatcher(cls) -> p.Dispatcher: """Materialize the canonical dispatcher implementation behind ``p.Dispatcher``.""" diff --git a/src/flext_core/container.py b/src/flext_core/container.py index ccc5377a0b..117c310556 100644 --- a/src/flext_core/container.py +++ b/src/flext_core/container.py @@ -1,8 +1,10 @@ -"""Dependency injection container for the dispatcher-first CQRS stack. +"""Truthful dependency container for the dispatcher-first CQRS stack. -This module wraps dependency_injector behind a result-bearing API so handlers -and decorators can register/resolve dependencies without importing the -underlying infrastructure. Configuration stays isolated from dispatcher code. +The container is the registry of the core runtime. It keeps one bookkeeping of +named services, factories and resources, and every registration passes through +one private write path: an empty, duplicate or reserved name raises +``e.ValidationError`` instead of being ignored. Resolution surfaces ``r`` +(Result) so lookup failures stay explicit. Copyright (c) 2025 FLEXT Team. All rights reserved. SPDX-License-Identifier: MIT @@ -14,10 +16,9 @@ import sys import threading from collections.abc import Sequence +from functools import partial from typing import TYPE_CHECKING, ClassVar, Self, TypeGuard, cast, overload, override -from dependency_injector import containers as di_containers - from flext_core import ( FlextContext, FlextSettings, @@ -39,13 +40,13 @@ class FlextContainer(p.Container): - """Singleton DI container wrapping dependency_injector with result-bearing API. + """Process-wide registry of services, factories and resources. - Services and factories remain local to the container, keeping dispatcher and - domain code free from infrastructure imports. All operations surface - ``r`` (Result) so failures are explicit. Thread-safe initialization - guarantees one global instance for runtime usage while allowing scoped - containers in tests. + One mapping holds every registration; the core runtime names + (``c.CONTAINER_RESERVED_NAMES``) are written only by the container itself + and stay out of ``has``/``names``/``drop``. Scoped containers built with + ``scope`` inherit the public registrations and bind their own core + services to their own settings and context. """ _global_instance: Self | None = None @@ -62,23 +63,11 @@ class FlextContainer(p.Container): _user_overrides: m.ConfigMap - _di_bridge: di_containers.DeclarativeContainer - - _di_services: di_containers.DynamicContainer - - _di_resources: di_containers.DynamicContainer - - _di_container: di_containers.DynamicContainer - - _di_initialized: bool = False + _initialized: bool = False - _services: MutableMapping[str, m.ServiceRegistration] - - _factories: MutableMapping[str, m.FactoryRegistration] - - _resources: MutableMapping[str, m.ResourceRegistration] - - _internal_registrations: set[str] + _registrations: MutableMapping[ + str, m.ServiceRegistration | m.FactoryRegistration | m.ResourceRegistration + ] _global_config: m.ContainerConfig @@ -92,6 +81,16 @@ def __new__(cls, *, registration: m.ServiceRegistrationSpec | None = None) -> Se cls._global_instance = instance return cls._global_instance + def __init__( + self, *, registration: m.ServiceRegistrationSpec | None = None + ) -> None: + """Initialize the singleton once; later calls apply the explicit spec.""" + if not self._initialized: + self.initialize_registrations(registration=registration) + elif registration is not None: + self._apply_explicit_bootstrap(registration) + self._write_spec(registration) + @property @override def settings(self) -> p.Settings: @@ -104,13 +103,6 @@ def context(self) -> p.Context: """Execution context bound to this container.""" return self._context - @property - @override - def provide(self) -> Callable[[str], t.RegisterableService]: - """Dependency-injector Provide helper scoped to the bridge.""" - provider: Callable[[str], t.RegisterableService] = self._di_bridge.provide - return provider - @classmethod def reset_for_testing(cls) -> None: """Reset singleton instance for testing purposes.""" @@ -138,29 +130,103 @@ def _matches_service_type[T: t.RegisterableService]( """Narrow a resolved service through its structural runtime type.""" return isinstance(value, expected) - def _resolve_callable[T: t.RegisterableService]( - self, callable_obj: t.FactoryCallable, kind: str, type_cls: type[T] | None - ) -> p.Result[T] | p.Result[t.RegisterableService]: - """Invoke a factory/resource callable and narrow to ``type_cls`` if given.""" + @staticmethod + def _service_record( + name: str, value: m.ServiceRegistration | t.RegisterableService + ) -> m.ServiceRegistration: + """Build (or keep) the validated record of an object-backed service.""" + if isinstance(value, m.ServiceRegistration): + return value + return m.ServiceRegistration( + name=name, service=value, service_type=u.type_name(value) + ) + + @staticmethod + def _factory_record( + name: str, value: m.FactoryRegistration | t.FactoryCallable + ) -> m.FactoryRegistration: + """Build (or keep) the validated record of a factory.""" + if isinstance(value, m.FactoryRegistration): + return value + return m.FactoryRegistration(name=name, factory=value) + + @staticmethod + def _resource_record( + name: str, value: m.ResourceRegistration | t.ResourceCallable + ) -> m.ResourceRegistration: + """Build (or keep) the validated record of a resource.""" + if isinstance(value, m.ResourceRegistration): + return value + return m.ResourceRegistration(name=name, factory=value) + + def _write( + self, + name: str, + build: Callable[ + [], m.ServiceRegistration | m.FactoryRegistration | m.ResourceRegistration + ], + *, + internal: bool = False, + ) -> Self: + """Apply the registration rules and store one validated record. + + This is the only path that mutates the registrations. Public writes + reject empty, reserved and duplicate names; the container's own core + writes (``internal``) may only target reserved names. + """ + if not name: + raise e.ValidationError(c.ERR_CONTAINER_NAME_EMPTY) + reserved = name in c.CONTAINER_RESERVED_NAMES + if reserved != internal: + raise e.ValidationError(c.ERR_CONTAINER_NAME_RESERVED.format(name=name)) + if not internal and name in self._registrations: + raise e.ValidationError(c.ERR_CONTAINER_NAME_DUPLICATE.format(name=name)) + try: + record = build() + except ValueError as exc: + raise e.ValidationError( + c.ERR_CONTAINER_REGISTRATION_FAILED.format(name=name, reason=exc) + ) from exc + self._registrations[name] = record + return self + + def _write_spec(self, spec: m.ServiceRegistrationSpec) -> None: + """Write every service, factory and resource declared by a spec.""" + for name, service in (spec.services or {}).items(): + _ = self._write(name, partial(self._service_record, name, service)) + for name, factory in (spec.factories or {}).items(): + _ = self._write(name, partial(self._factory_record, name, factory)) + for name, resource in (spec.resources or {}).items(): + _ = self._write(name, partial(self._resource_record, name, resource)) + + @override + def bind(self, name: str, impl: t.RegisterableService) -> Self: + """Bind a concrete service instance or value.""" + return self._write(name, partial(self._service_record, name, impl)) + + @override + def factory(self, name: str, impl: t.FactoryCallable) -> Self: + """Bind a factory callable invoked on every resolve.""" + return self._write(name, partial(self._factory_record, name, impl)) + + @override + def resource(self, name: str, impl: t.ResourceCallable) -> Self: + """Bind a resource factory invoked on every resolve.""" + return self._write(name, partial(self._resource_record, name, impl)) + + def _resolve_callable( + self, callable_obj: t.FactoryCallable, kind: str + ) -> p.Result[t.RegisterableService]: + """Invoke a factory/resource callable and validate what it produced.""" try: resolved = callable_obj() + _ = u.normalize_registerable_service(resolved) except c.EXC_BROAD_RUNTIME as exc: return r[t.RegisterableService].from_result( e.fail_operation( f"resolve {kind}", exc, result_type=r[t.RegisterableService] ) ) - if type_cls is not None: - if self._matches_service_type(resolved, type_cls): - return r[T].ok(resolved) - return cast( - "p.Result[T] | p.Result[t.RegisterableService]", - r[T].from_result( - e.fail_type_mismatch( - type_cls.__name__, type(resolved).__name__, result_type=r[T] - ) - ), - ) return r[t.RegisterableService].ok(resolved) @overload @@ -177,43 +243,30 @@ def resolve( def resolve[T: t.RegisterableService]( self, name: str, *, type_cls: type[T] | None = None ) -> p.Result[T] | p.Result[t.RegisterableService]: - """Resolve a registered service or factory by name.""" - service_registration = self._services.get(name) - callable_registration = next( - ( - (kind, registration.factory) - for kind, registrations in ( - ("factory", self._factories), - ("resource", self._resources), + """Resolve a registered service, factory or resource by name.""" + match self._registrations.get(name): + case None: + return r[t.RegisterableService].from_result( + e.fail_not_found("service", name) + ) + case m.ServiceRegistration() as record: + result = r[t.RegisterableService].ok(record.service) + case m.FactoryRegistration() as record: + result = self._resolve_callable(record.factory, "factory") + case record: + result = self._resolve_callable(record.factory, "resource") + if type_cls is None or result.failure: + return result + if self._matches_service_type(result.value, type_cls): + return r[T].ok(result.value) + return cast( + "p.Result[T]", + r[T].from_result( + e.fail_type_mismatch( + type_cls.__name__, type(result.value).__name__, result_type=r[T] ) - if (registration := registrations.get(name)) is not None ), - None, ) - if service_registration is not None: - service = service_registration.service - if type_cls is None: - result: p.Result[T] | p.Result[t.RegisterableService] = r[ - t.RegisterableService - ].ok(service) - elif isinstance(service, type_cls): - result = r[T].ok(service) - else: - result = cast( - "p.Result[T]", - r[T].from_result( - e.fail_type_mismatch(type_cls.__name__, type(service).__name__) - ), - ) - elif callable_registration is not None: - kind, callable_obj = callable_registration - result = self._resolve_callable(callable_obj, kind, type_cls) - else: - result = cast( - "p.Result[t.RegisterableService]", - r[t.RegisterableService].from_result(e.fail_not_found("service", name)), - ) - return result @override def snapshot(self) -> m.ConfigMap: @@ -226,212 +279,69 @@ def snapshot(self) -> m.ConfigMap: @override def has(self, name: str) -> bool: """Return whether a public service, factory, or resource is registered.""" - return ( - (name in self._services and name not in self._internal_registrations) - or (name in self._factories and name not in self._internal_registrations) - or (name in self._resources and name not in self._internal_registrations) - ) + return name in self._registrations and name not in c.CONTAINER_RESERVED_NAMES - def initialize_di_components(self) -> None: - """Initialize DI components (bridge, services, resources, container).""" - self._di_bridge, self._di_services, self._di_resources = ( - u.DependencyIntegration.create_layered_bridge() - ) - self._di_container = di_containers.DynamicContainer() - config_provider = self._di_bridge.settings - if not hasattr(config_provider, "override"): - error_msg = "Bridge settings provider missing" - raise TypeError(error_msg) - self._di_container.settings = config_provider - self._di_initialized = True + @override + def names(self) -> t.StrSequence: + """List the public services, factories, and resources.""" + return [name for name in self._registrations if self.has(name)] def initialize_registrations( self, *, registration: m.ServiceRegistrationSpec | None = None ) -> None: - """Initialize service registrations and configuration.""" + """Reset the registrations from a spec and register the core services.""" spec = registration or m.ServiceRegistrationSpec() - self._services = dict(u.normalize_service_registrations(spec.services) or {}) - self._factories = dict(u.normalize_factory_registrations(spec.factories) or {}) - self._resources = dict(u.normalize_resource_registrations(spec.resources) or {}) - self._internal_registrations = { - name - for name in self._services - if name - in { - str(c.Directory.CONFIG), - str(c.ServiceName.LOGGER), - c.FIELD_CONTEXT, - str(c.ServiceName.COMMAND_BUS), - } - } + self._registrations = {} self._global_config = spec.container_config or m.ContainerConfig() - user_overrides_input = spec.user_overrides - if isinstance(user_overrides_input, m.ConfigMap): - self._user_overrides = user_overrides_input - elif user_overrides_input: - self._user_overrides = m.ConfigMap( + overrides = spec.user_overrides + self._user_overrides = ( + overrides + if isinstance(overrides, m.ConfigMap) + else m.ConfigMap( root={ k: list(v) if isinstance(v, Sequence) and not isinstance(v, str | bytes) else v - for k, v in user_overrides_input.items() + for k, v in (overrides or {}).items() } ) - else: - self._user_overrides = m.ConfigMap(root={}) + ) self._config = ( spec.settings.clone() if spec.settings is not None else self._settings_type.fetch_global() ) - context = spec.context - self._context = context if context is not None else self._context_type.create() - - @override - def names(self) -> t.StrSequence: - """List explicitly registered services, factories, and resources.""" - return [ - name - for name in ( - list(self._services.keys()) - + list(self._factories.keys()) - + list(self._resources.keys()) - ) - if name not in self._internal_registrations - ] - - @override - def bind(self, name: str, impl: t.RegisterableService) -> Self: - """Bind a concrete service instance or value.""" - if not name or self.has(name): - return self - self._internal_registrations.discard(name) - try: - self._update_registered_object_service(name, impl) - except c.EXC_ATTR_RUNTIME_TYPE as exc: - del self._services[name] - raise e.ValidationError( - c.ERR_CONTAINER_REGISTRATION_FAILED.format(name=name, reason=exc) - ) from exc - return self - - @override - def factory(self, name: str, impl: t.FactoryCallable) -> Self: - """Bind a factory callable.""" - if not name: - return self - if self.has(name): - return self - self._internal_registrations.discard(name) - for di_ns in (self._di_services, self._di_resources): - if hasattr(di_ns, name): - delattr(di_ns, name) - - def normalized_factory() -> t.RegisterableService: - raw = impl() - try: - _ = u.normalize_registerable_service(raw) - except ValueError as exc: - raise ValueError( - c.ERR_CONTAINER_FACTORY_INVALID_REGISTERABLE.format(name=name) - ) from exc - return raw - - self._factories[name] = m.FactoryRegistration( - name=name, factory=normalized_factory - ) - try: - u.DependencyIntegration.register_factory( - self._di_services, - name, - normalized_factory, - cache=self._global_config.enable_factory_caching, - ) - setattr(self._di_bridge, name, getattr(self._di_services, name)) - except c.EXC_ATTR_RUNTIME_TYPE as exc: - del self._factories[name] - raise e.ValidationError( - c.ERR_CONTAINER_REGISTRATION_FAILED.format(name=name, reason=exc) - ) from exc - return self - - @override - def resource(self, name: str, impl: t.ResourceCallable) -> Self: - """Bind a lifecycle-managed resource factory.""" - if not name: - return self - if self.has(name): - return self - self._internal_registrations.discard(name) - for di_ns in (self._di_services, self._di_resources): - if hasattr(di_ns, name): - delattr(di_ns, name) - self._resources[name] = m.ResourceRegistration(name=name, factory=impl) - try: - u.DependencyIntegration.register_resource(self._di_resources, name, impl) - setattr(self._di_bridge, name, getattr(self._di_resources, name)) - except c.EXC_ATTR_RUNTIME_TYPE as exc: - del self._resources[name] - raise e.ValidationError( - c.ERR_CONTAINER_REGISTRATION_FAILED.format(name=name, reason=exc) - ) from exc - return self - - def _update_registered_object_service( - self, name: str, service: t.RegisterableService - ) -> None: - """Replace or insert an object-backed service across local and DI state.""" - self._services[name] = m.ServiceRegistration( - name=name, service=service, service_type=u.type_name(service) + self._context = ( + spec.context if spec.context is not None else self._context_type.create() ) - for di_ns in (self._di_services, self._di_resources): - if hasattr(di_ns, name): - delattr(di_ns, name) - u.DependencyIntegration.register_object(self._di_services, name, service) - setattr(self._di_bridge, name, getattr(self._di_services, name)) - - def register_existing_providers(self) -> None: - """Hydrate the dynamic container with current registrations.""" - cache = self._global_config.enable_factory_caching - for name, reg in self._services.items(): - if not ( - hasattr(self._di_services, name) or hasattr(self._di_container, name) - ): - u.DependencyIntegration.register_object( - self._di_services, name, reg.service - ) - for name, reg in self._factories.items(): - if not ( - hasattr(self._di_services, name) or hasattr(self._di_container, name) - ): - u.DependencyIntegration.register_factory( - self._di_services, name, reg.factory, cache=cache - ) - for name, reg in self._resources.items(): - if not ( - hasattr(self._di_resources, name) or hasattr(self._di_container, name) - ): - u.DependencyIntegration.register_resource( - self._di_resources, name, reg.factory - ) + self._write_spec(spec) + self.register_core_services() + self._initialized = True @override def register_core_services(self) -> None: - """Auto-register core services for easy DI access.""" - if str(c.Directory.CONFIG) not in self._internal_registrations: - self.bind(c.Directory.CONFIG, self._config) - self._internal_registrations.add(str(c.Directory.CONFIG)) - if str(c.ServiceName.LOGGER) not in self._internal_registrations: - self.factory( - c.ServiceName.LOGGER, lambda: u.fetch_logger(c.LOGGER_NAME_FLEXT_CORE) - ) - self._internal_registrations.add(str(c.ServiceName.LOGGER)) - if c.FIELD_CONTEXT not in self._internal_registrations: - self.bind(c.FIELD_CONTEXT, self._context) - self._internal_registrations.add(c.FIELD_CONTEXT) - if str(c.ServiceName.COMMAND_BUS) not in self._internal_registrations: - self.bind(c.ServiceName.COMMAND_BUS, u.build_dispatcher()) - self._internal_registrations.add(str(c.ServiceName.COMMAND_BUS)) + """Register the reserved core services that are not registered yet.""" + core: tuple[tuple[str, Callable[[], m.ServiceRegistration | m.FactoryRegistration]], ...] = ( + (c.Directory.CONFIG, partial(self._service_record, c.Directory.CONFIG, self._config)), + ( + c.ServiceName.LOGGER, + partial( + self._factory_record, + c.ServiceName.LOGGER, + partial(u.fetch_logger, c.LOGGER_NAME_FLEXT_CORE), + ), + ), + (c.FIELD_CONTEXT, partial(self._service_record, c.FIELD_CONTEXT, self._context)), + ( + c.ServiceName.COMMAND_BUS, + lambda: self._service_record( + c.ServiceName.COMMAND_BUS, u.build_dispatcher() + ), + ), + ) + for name, build in core: + if name not in self._registrations: + _ = self._write(name, build, internal=True) @override def scope( @@ -440,101 +350,61 @@ def scope( subproject: str | None = None, registration: m.ServiceRegistrationSpec | None = None, ) -> Self: - """Create an isolated container scope with optional overrides.""" - scope_registration = registration or m.ServiceRegistrationSpec() - settings_source = ( - scope_registration.settings - if scope_registration.settings is not None - else self._config - ) - base_config: p.Settings = settings_source.clone() - scoped_context = ( - self.context.clone() - if scope_registration.context is None - else scope_registration.context - ) + """Create an isolated scope inheriting the public registrations. + + Registrations declared by ``registration`` override inherited names; + the scope binds its own core services to its own settings and context. + """ + spec = registration or m.ServiceRegistrationSpec() + settings_source = spec.settings if spec.settings is not None else self._config + scoped_context = self.context.clone() if spec.context is None else spec.context if subproject: _ = scoped_context.set("subproject", subproject) - cloned_services = { - name: reg.model_copy(deep=False) for name, reg in self._services.items() - } - cloned_factories = { - name: reg.model_copy(deep=False) for name, reg in self._factories.items() - } - cloned_resources = { - name: reg.model_copy(deep=False) for name, reg in self._resources.items() - } - cloned_services.update( - u.normalize_service_registrations(scope_registration.services) or {} - ) - cloned_factories.update( - u.normalize_factory_registrations(scope_registration.factories) or {} - ) - cloned_resources.update( - u.normalize_resource_registrations(scope_registration.resources) or {} - ) + inherited = {name: self._registrations[name] for name in self.names()} scoped = u.create_instance(self.__class__) - scoped.initialize_di_components() scoped.initialize_registrations( - registration=u.normalize_service_registration_spec( - m.ServiceRegistrationSpec( - settings=base_config, - context=scoped_context, - services=cloned_services, - factories=cloned_factories, - resources=cloned_resources, - user_overrides=self._user_overrides.model_copy(), - container_config=self._global_config.model_copy(deep=True), - ) + registration=m.ServiceRegistrationSpec( + settings=settings_source.clone(), + context=scoped_context, + services={ + **{ + name: record + for name, record in inherited.items() + if isinstance(record, m.ServiceRegistration) + }, + **(spec.services or {}), + }, + factories={ + **{ + name: record + for name, record in inherited.items() + if isinstance(record, m.FactoryRegistration) + }, + **(spec.factories or {}), + }, + resources={ + **{ + name: record + for name, record in inherited.items() + if isinstance(record, m.ResourceRegistration) + }, + **(spec.resources or {}), + }, + user_overrides=self._user_overrides.model_copy(), + container_config=self._global_config.model_copy(deep=True), ) ) - scoped.sync_config_to_di() - scoped.register_existing_providers() - scoped.register_core_services() return scoped - def sync_config_to_di(self) -> None: - """Synchronize FlextSettings to DI providers.Configuration.""" - config_dict = self._global_config.model_dump() - config_map = m.ConfigMap( - root={k: u.normalize_to_container(v) for k, v in config_dict.items()} - ) - _ = u.DependencyIntegration.bind_configuration(self._di_container, config_map) - @override def drop(self, name: str) -> p.Result[bool]: - """Remove a service or factory registration by name.""" - removed = False - if name in self._services: - del self._services[name] - removed = True - if name in self._factories: - del self._factories[name] - removed = True - if name in self._resources: - del self._resources[name] - removed = True - for di_ns in (self._di_services, self._di_resources): - if hasattr(di_ns, name): - delattr(di_ns, name) - if removed: - return r[bool].ok(True) - return r[bool].from_result( - e.fail_not_found("service", name, result_type=r[bool]) - ) - - @override - def wire( - self, - *, - modules: t.SequenceOf[ModuleType] | None = None, - packages: t.StrSequence | None = None, - classes: t.SequenceOf[type] | None = None, - ) -> None: - """Wire modules/packages to the DI bridge for @inject/Provide usage.""" - u.DependencyIntegration.wire( - self._di_container, modules=modules, packages=packages, classes=classes - ) + """Remove a public service, factory, or resource registration by name.""" + if not self.has(name): + return r[bool].from_result( + e.fail_not_found("service", name, result_type=r[bool]) + ) + del self._registrations[name] + return r[bool].ok(True) @override def dispatcher(self) -> p.Result[p.Dispatcher]: @@ -554,21 +424,6 @@ def dispatcher(self) -> p.Result[p.Dispatcher]: ) ) - def __init__( - self, *, registration: m.ServiceRegistrationSpec | None = None - ) -> None: - """Initialize the singleton container (idempotent).""" - if self._di_initialized: - init_registration = registration or m.ServiceRegistrationSpec() - self._apply_explicit_bootstrap(init_registration) - self.register_core_services() - return - self.initialize_di_components() - self.initialize_registrations(registration=registration) - self.sync_config_to_di() - self.register_existing_providers() - self.register_core_services() - @classmethod def shared( cls, @@ -577,77 +432,69 @@ def shared( context: p.Context | None = None, auto_register_factories: bool = False, ) -> Self: - """Return the canonical shared container instance.""" + """Return the canonical shared container instance. + + ``auto_register_factories`` registers every ``@d.factory()`` function + of the calling module; a caller that cannot be resolved to an imported + module raises ``e.ValidationError``. + """ instance = cls() if settings is not None or context is not None: instance._apply_explicit_bootstrap( m.ServiceRegistrationSpec(settings=settings, context=context) ) if auto_register_factories: - frame = inspect.currentframe() - caller_module = FlextContainer._resolve_caller_module(frame) - if caller_module is not None: - FlextContainer._auto_register_module_factories(instance, caller_module) + caller_module = cls._resolve_caller_module(inspect.currentframe()) + cls._auto_register_module_factories(instance, caller_module) return instance @staticmethod - def _resolve_caller_module(frame: FrameType | None) -> ModuleType | None: - """Resolve the module that called the factory via frame introspection.""" - if not frame or not frame.f_back: - return None - module_name = str(frame.f_back.f_globals.get("__name__", "__main__")) - return sys.modules.get(module_name) + def _resolve_caller_module(frame: FrameType | None) -> ModuleType: + """Resolve the imported module that called ``shared`` or raise.""" + caller = frame.f_back if frame is not None else None + module_name = caller.f_globals.get("__name__") if caller is not None else None + module = sys.modules.get(module_name) if isinstance(module_name, str) else None + if module is None: + raise e.ValidationError(c.ERR_CONTAINER_CALLER_UNRESOLVED) + return module @staticmethod def _auto_register_module_factories( instance: p.Container, caller_module: ModuleType ) -> None: - """Scan module for @d.factory() functions and register them.""" - factories = u.scan_module(caller_module) + """Register every ``@d.factory()`` function of a module. + + Each discovered function passes the validated factory record, so a + non-callable, duplicate or reserved factory raises ``e.ValidationError``. + """ module_symbols = vars(caller_module) - for factory_name, factory_config in factories: - factory_func = module_symbols.get(factory_name) - if factory_func is None or not callable(factory_func): - continue - impl: t.FactoryCallable = cast("t.FactoryCallable", factory_func) + for factory_name, factory_config in u.scan_module(caller_module): + impl = cast("t.FactoryCallable", module_symbols[factory_name]) _ = instance.factory(factory_config.name, impl) def _apply_explicit_bootstrap( self, registration: m.ServiceRegistrationSpec ) -> None: - """Apply explicit bootstrap overrides to an existing singleton instance.""" + """Rebind the core settings and context of an existing container.""" if registration.settings is not None: self._config = registration.settings - self._update_registered_object_service( - c.Directory.CONFIG, registration.settings + _ = self._write( + c.Directory.CONFIG, + partial(self._service_record, c.Directory.CONFIG, self._config), + internal=True, ) - self.sync_config_to_di() if registration.context is not None: self._context = registration.context - self._update_registered_object_service( - c.FIELD_CONTEXT, registration.context + _ = self._write( + c.FIELD_CONTEXT, + partial(self._service_record, c.FIELD_CONTEXT, self._context), + internal=True, ) @override def clear(self) -> None: - """Clear all service and factory registrations.""" - names_to_clear = { - *self._services.keys(), - *self._factories.keys(), - *self._resources.keys(), - str(c.Directory.CONFIG), - str(c.ServiceName.LOGGER), - c.FIELD_CONTEXT, - str(c.ServiceName.COMMAND_BUS), - } - for name in names_to_clear: - for di_ns in (self._di_services, self._di_resources): - if hasattr(di_ns, name): - delattr(di_ns, name) - self._services.clear() - self._factories.clear() - self._resources.clear() - self._internal_registrations.clear() + """Clear every registration and re-register the core services.""" + self._registrations.clear() self._config = self._settings_type.fetch_global() self.register_core_services() @@ -659,10 +506,9 @@ def apply(self, settings: t.UserOverridesMapping | None = None) -> Self: merged = self._user_overrides.model_copy() merged.update({k: u.normalize_to_container(v) for k, v in settings.items()}) self._user_overrides = merged - self._global_config = self._global_config.model_copy( - update=dict(merged), deep=True + self._global_config = m.ContainerConfig.model_validate( + self._global_config.model_dump() | dict(merged) ) - self.sync_config_to_di() return self diff --git a/src/flext_core/context.py b/src/flext_core/context.py index 88a9e399f8..cbd5ff5638 100644 --- a/src/flext_core/context.py +++ b/src/flext_core/context.py @@ -18,7 +18,7 @@ from datetime import datetime, timedelta from typing import TYPE_CHECKING, Annotated, ClassVar, Self -from flext_core import c, m, p, r, t, u +from flext_core import c, e, m, p, r, t, u # NOTE (multi-agent): mro-i6nq.12 — Generator is annotation-only; importing it # under TYPE_CHECKING keeps the public runtime facade graph lazy. @@ -160,12 +160,11 @@ def register_service( ) -> p.Result[bool]: """Register a named service in the global container.""" container = FlextContext.resolve_container() - _ = container.bind(service_name, service) - return ( - r[bool].ok(True) - if container.has(service_name) - else r[bool].fail(f"Service '{service_name}' was not registered") - ) + try: + _ = container.bind(service_name, service) + except e.ValidationError as exc: + return r[bool].fail_op("register service", exc) + return r[bool].ok(True) @staticmethod def resolve_correlation_id() -> str | None: diff --git a/src/flext_core/registry.py b/src/flext_core/registry.py index ccf0b8e9bb..7ca28058a5 100644 --- a/src/flext_core/registry.py +++ b/src/flext_core/registry.py @@ -279,17 +279,25 @@ def register(self, name: str, service: t.RegistrablePlugin) -> p.Result[bool]: service: Service instance to register Returns: - r[bool]: Success (True) if registered or failure with error details. + r[bool]: Success (True) when registered, or when the same value is + already registered under ``name``; failure for a different value + under a registered name, or an empty or reserved name. """ - was_registered = self.container.has(name) normalized_service = self._normalize_registration_impl(service) - _ = self.container.bind(name, normalized_service) - if was_registered or self.container.has(name): - return r[bool].ok(True) - return r[bool].fail_op( - "register service in registry", f"Service '{name}' was not registered" - ) + if self.container.has(name): + existing = self.container.resolve(name) + if existing.success and existing.value == normalized_service: + return r[bool].ok(True) + return r[bool].fail_op( + "register service in registry", + c.ERR_CONTAINER_NAME_DUPLICATE.format(name=name), + ) + try: + _ = self.container.bind(name, normalized_service) + except e.ValidationError as exc: + return e.fail_operation("register service in registry", exc) + return r[bool].ok(True) def register_bindings( self, bindings: t.MappingKV[t.RegistryBindingKey, t.DispatchableHandler] diff --git a/src/flext_core/runtime.py b/src/flext_core/runtime.py index 9b2350565a..2f067fe840 100644 --- a/src/flext_core/runtime.py +++ b/src/flext_core/runtime.py @@ -6,11 +6,11 @@ from __future__ import annotations -from ._runtime import FlextRuntimeContainer, FlextRuntimeDependencyIntegration +from ._runtime import FlextRuntimeContainer -class FlextRuntime(FlextRuntimeContainer, FlextRuntimeDependencyIntegration): - """Expose runtime normalization, DI, and validation helpers.""" +class FlextRuntime(FlextRuntimeContainer): + """Expose runtime normalization and validation helpers.""" __all__: list[str] = ["FlextRuntime"] diff --git a/tests/_constants/other.py b/tests/_constants/other.py index e19ba2a947..c3915b0c53 100644 --- a/tests/_constants/other.py +++ b/tests/_constants/other.py @@ -33,6 +33,11 @@ class TestsFlextConstantsOther: "examples.ex_07_flext_exceptions", "ex_07_flext_exceptions.py", ), + ( + "ex_08_flext_container", + "examples.ex_08_flext_container", + "ex_08_flext_container.py", + ), ( "ex_11_flext_service", "examples.ex_11_flext_service", diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 9caf3e020a..4ccf72915d 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -14,6 +14,7 @@ from flext_core import FlextContainer from flext_core.__version__ import __version__ +from tests import e from tests.protocols import p from tests.utilities import u @@ -151,15 +152,16 @@ def test_container_resolve_unknown_name_fails_with_error( tm.that(resolved.error, none=False) tm.that(tm.not_none(resolved.error), has="does_not_exist") - def test_container_bind_is_idempotent_for_existing_name( + def test_container_bind_rejects_existing_name( self, clean_container: p.Container ) -> None: - """Re-binding an existing name preserves the first value (no overwrite).""" + """Re-binding an existing name raises and preserves the first value.""" # Arrange _ = clean_container.bind("svc", "first") # Act - _ = clean_container.bind("svc", "second") + with pytest.raises(e.ValidationError, match="svc"): + _ = clean_container.bind("svc", "second") resolved = clean_container.resolve("svc") # Assert diff --git a/tests/unit/test_container_lifecycle.py b/tests/unit/test_container_lifecycle.py index 5226147781..18e200c843 100644 --- a/tests/unit/test_container_lifecycle.py +++ b/tests/unit/test_container_lifecycle.py @@ -94,7 +94,7 @@ def test_registered_services_resolve_to_their_bound_values( """Resolve returns the exact value/instance that was registered.""" container = clean_container _ = container.bind("cache", "redis") - _ = container.factory("logger", u.Tests.create_factory("logger-instance")) + _ = container.factory("audit_log", u.Tests.create_factory("audit-instance")) tm.ok( container.resolve("cache", type_cls=str), @@ -102,8 +102,8 @@ def test_registered_services_resolve_to_their_bound_values( msg="Bound value must resolve unchanged", ) tm.ok( - container.resolve("logger", type_cls=str), - eq="logger-instance", + container.resolve("audit_log", type_cls=str), + eq="audit-instance", msg="Factory product must resolve to the produced value", ) tm.that(len(container.names()), eq=2, msg="Both registrations must be counted") @@ -171,8 +171,8 @@ def test_scope_creates_isolated_child_without_polluting_parent( """ scoped = clean_container.scope( subproject="unit", - registration=u.normalize_service_registration_spec( - m.ServiceRegistrationSpec(services={"scoped_service": "scoped-value"}) + registration=m.ServiceRegistrationSpec( + services={"scoped_service": "scoped-value"} ), ) diff --git a/tests/unit/test_container_properties.py b/tests/unit/test_container_properties.py index 9300a351ba..2bb52abbe3 100644 --- a/tests/unit/test_container_properties.py +++ b/tests/unit/test_container_properties.py @@ -9,6 +9,8 @@ from hypothesis import assume, given, settings, strategies as st from flext_core.container import FlextContainer +from tests import e +from tests.constants import c class TestsFlextCoreContainerProperties: @@ -20,17 +22,6 @@ class TestsFlextCoreContainerProperties: never internal registries or DI wiring. """ - _RESERVED_CONTAINER_ATTRS: frozenset[str] = frozenset({ - "override", - "overridden", - "providers", - "reset_override", - "reset_last_overriding", - "set_providers", - "declarative_parent", - "settings", - }) - @pytest.fixture def container(self) -> Iterator[FlextContainer]: """Yield the shared container and reset the singleton afterwards.""" @@ -131,12 +122,14 @@ def test_clear_removes_all_user_registrations( # -- invariants -------------------------------------------------------- - def test_bind_is_idempotent_first_write_wins( + def test_bind_duplicate_raises_and_first_write_stays( self, container: FlextContainer ) -> None: - """Re-binding an existing name keeps the original value.""" + """Re-binding an existing name raises and keeps the original value.""" _ = container.bind("dup", 1) - _ = container.bind("dup", 2) + + with pytest.raises(e.ValidationError, match="dup"): + _ = container.bind("dup", 2) tm.ok(container.resolve("dup", type_cls=int), eq=1) @@ -173,7 +166,7 @@ def test_register_get_roundtrip_property(self, name: str) -> None: """For any valid name, factory registration then resolution roundtrips.""" container = FlextContainer.shared() sanitized = "".join(ch for ch in name if ch.isalnum()) or "svc" - assume(sanitized not in self._RESERVED_CONTAINER_ATTRS) + assume(sanitized not in c.CONTAINER_RESERVED_NAMES) _ = container.factory(sanitized, lambda value=sanitized: value) diff --git a/tests/unit/test_container_registration.py b/tests/unit/test_container_registration.py index 0149df5c14..fd45cafc16 100644 --- a/tests/unit/test_container_registration.py +++ b/tests/unit/test_container_registration.py @@ -8,6 +8,8 @@ from flext_tests import tm from flext_core.container import FlextContainer +from tests import e +from tests.constants import c from tests.models import m from tests.protocols import p from tests.utilities import u @@ -67,31 +69,25 @@ def test_bind_registers_and_resolves_service( clean_container.resolve(scenario.name), expected_value=scenario.service ) - def test_bind_duplicate_name_preserves_first_binding( + def test_bind_duplicate_name_raises_and_keeps_first_binding( self, clean_container: p.Container ) -> None: - """Re-binding an existing name is a no-op; the first value wins.""" + """Re-binding a registered name raises; the first value stays bound.""" _ = clean_container.bind("service1", "value1") - _ = clean_container.bind("service1", "value2") + + with pytest.raises(e.ValidationError, match="service1"): + _ = clean_container.bind("service1", "value2") u.Tests.assert_success( clean_container.resolve("service1"), expected_value="value1" ) - def test_bind_empty_name_is_rejected(self, clean_container: p.Container) -> None: - """An empty service name is never registered.""" - result = clean_container.bind("", "service") + def test_bind_empty_name_raises(self, clean_container: p.Container) -> None: + """An empty service name raises and registers nothing.""" + with pytest.raises(e.ValidationError, match=c.ERR_CONTAINER_NAME_EMPTY): + _ = clean_container.bind("", "service") - tm.that( - result is clean_container, - eq=True, - msg="bind must stay fluent even when rejecting an empty name", - ) - tm.that( - clean_container.has(""), - eq=False, - msg="Empty-named service must not be registered", - ) + tm.that(clean_container.names(), empty=True) def test_resolve_unknown_name_fails(self, clean_container: p.Container) -> None: """Resolving an unregistered name yields a failing result naming it.""" @@ -146,31 +142,25 @@ def test_factory_is_invoked_lazily_on_resolve( msg="Factory must be invoked to produce the resolved value", ) - def test_factory_duplicate_name_preserves_first_factory( + def test_factory_duplicate_name_raises_and_keeps_first_factory( self, clean_container: p.Container ) -> None: - """Re-registering a factory name keeps the original callable.""" + """Re-registering a factory name raises; the original callable stays.""" clean_container.factory("factory1", u.Tests.create_factory("value1")) - clean_container.factory("factory1", u.Tests.create_factory("value2")) + + with pytest.raises(e.ValidationError, match="factory1"): + clean_container.factory("factory1", u.Tests.create_factory("value2")) u.Tests.assert_success( clean_container.resolve("factory1"), expected_value="value1" ) - def test_factory_empty_name_is_rejected(self, clean_container: p.Container) -> None: - """An empty factory name is never registered.""" - result = clean_container.factory("", u.Tests.create_factory("value")) + def test_factory_empty_name_raises(self, clean_container: p.Container) -> None: + """An empty factory name raises and registers nothing.""" + with pytest.raises(e.ValidationError, match=c.ERR_CONTAINER_NAME_EMPTY): + clean_container.factory("", u.Tests.create_factory("value")) - tm.that( - clean_container.has(""), - eq=False, - msg="Empty-named factory must not be registered", - ) - tm.that( - result is clean_container, - eq=True, - msg="factory must stay fluent when rejecting an empty name", - ) + tm.that(clean_container.names(), empty=True) def test_drop_removes_registered_service( self, clean_container: p.Container diff --git a/tests/unit/test_container_truthful.py b/tests/unit/test_container_truthful.py new file mode 100644 index 0000000000..d2a79d6895 --- /dev/null +++ b/tests/unit/test_container_truthful.py @@ -0,0 +1,155 @@ +"""Behavioral tests for the truthful container write rules. + +Every registration passes one write path: empty, duplicate and reserved names +raise ``e.ValidationError``; the reserved core services stay private to the +container, including in scopes; factory auto-registration never skips. +""" + +from __future__ import annotations + +import sys +import types +from typing import TYPE_CHECKING + +import pytest +from flext_tests import d, tm + +from flext_core.container import FlextContainer +from tests import e +from tests.constants import c +from tests.models import m +from tests.protocols import p +from tests.utilities import u + +if TYPE_CHECKING: + from tests.typings import t + + +class TestsFlextCoreContainerTruthful: + """Exercise the rules of the single container write path.""" + + @staticmethod + def _shared_with_auto_registration() -> None: + _ = FlextContainer.shared(auto_register_factories=True) + + @pytest.mark.parametrize("name", sorted(c.CONTAINER_RESERVED_NAMES)) + def test_public_writes_reject_reserved_names( + self, name: str, clean_container: p.Container + ) -> None: + """bind, factory and resource refuse every core runtime name.""" + factory = u.Tests.create_factory("value") + for write in ( + lambda: clean_container.bind(name, "value"), + lambda: clean_container.factory(name, factory), + lambda: clean_container.resource(name, factory), + ): + with pytest.raises(e.ValidationError, match="reserved"): + _ = write() + tm.ok(clean_container.resolve(name)) + tm.that(clean_container.has(name), eq=False) + + def test_duplicate_is_rejected_across_registration_kinds( + self, clean_container: p.Container + ) -> None: + """A name bound as a service cannot be reused by a factory or resource.""" + factory = u.Tests.create_factory("other") + _ = clean_container.bind("shared_name", "service") + + with pytest.raises(e.ValidationError, match="shared_name"): + _ = clean_container.factory("shared_name", factory) + with pytest.raises(e.ValidationError, match="shared_name"): + _ = clean_container.resource("shared_name", factory) + + tm.ok(clean_container.resolve("shared_name"), eq="service") + + def test_resource_empty_name_raises(self, clean_container: p.Container) -> None: + """An empty resource name raises instead of being ignored.""" + with pytest.raises(e.ValidationError, match=c.ERR_CONTAINER_NAME_EMPTY): + _ = clean_container.resource("", u.Tests.create_factory("value")) + + def test_drop_of_reserved_name_fails_and_keeps_core_service( + self, clean_container: p.Container + ) -> None: + """Core services are not public, so drop reports them as not found.""" + tm.fail(clean_container.drop(c.ServiceName.LOGGER), has="logger") + tm.ok(clean_container.resolve(c.ServiceName.LOGGER)) + + def test_scope_keeps_logger_internal(self, clean_container: p.Container) -> None: + """Regression: a scope re-registers LOGGER as internal, never public.""" + _ = clean_container.bind("public", "value") + + scoped = clean_container.scope() + nested = scoped.scope() + + for container in (scoped, nested): + tm.that(list(container.names()), eq=["public"]) + tm.that(container.has(c.ServiceName.LOGGER), eq=False) + logger = tm.ok(container.resolve(c.ServiceName.LOGGER)) + tm.that(isinstance(logger, p.Logger), eq=True) + with pytest.raises(e.ValidationError, match="reserved"): + _ = container.factory( + c.ServiceName.LOGGER, u.Tests.create_factory("value") + ) + + def test_scope_binds_core_services_to_its_own_runtime( + self, clean_container: p.Container + ) -> None: + """The scoped settings and context services are the scope's own.""" + scoped = clean_container.scope(subproject="unit") + + tm.that( + tm.ok(scoped.resolve(c.Directory.CONFIG)) is scoped.settings, eq=True + ) + tm.that( + tm.ok(scoped.resolve(c.FIELD_CONTEXT)) is scoped.context, eq=True + ) + tm.that(scoped.settings is clean_container.settings, eq=False) + + def test_scope_spec_overrides_inherited_registration( + self, clean_container: p.Container + ) -> None: + """A scope declaration replaces the inherited value of the same name.""" + _ = clean_container.bind("mode", "parent") + + scoped = clean_container.scope( + registration=m.ServiceRegistrationSpec(services={"mode": "scoped"}) + ) + + tm.ok(scoped.resolve("mode"), eq="scoped") + tm.ok(clean_container.resolve("mode"), eq="parent") + + def test_auto_registration_without_imported_caller_raises( + self, clean_container: p.Container + ) -> None: + """A caller module absent from ``sys.modules`` is never skipped.""" + caller = types.FunctionType( + self._shared_with_auto_registration.__code__, + {"__name__": "tests_unimported_caller", "FlextContainer": FlextContainer}, + ) + + with pytest.raises(e.ValidationError, match="caller module"): + caller() + + tm.that(clean_container.names(), empty=True) + + def test_auto_registration_registers_caller_module_factories( + self, clean_container: p.Container + ) -> None: + """Every ``@d.factory()`` function of the calling module is registered.""" + module = types.ModuleType("tests_auto_registration_caller") + + @d.factory("made") + def build() -> t.JsonValue: + return "made-value" + + module.__dict__.update(build=build, FlextContainer=FlextContainer) + caller = types.FunctionType( + self._shared_with_auto_registration.__code__, module.__dict__ + ) + sys.modules[module.__name__] = module + try: + caller() + finally: + del sys.modules[module.__name__] + + tm.ok(clean_container.resolve("made"), eq="made-value") diff --git a/tests/unit/test_lazy_exports.py b/tests/unit/test_lazy_exports.py index 72f6ccc9cf..4878916b1d 100644 --- a/tests/unit/test_lazy_exports.py +++ b/tests/unit/test_lazy_exports.py @@ -80,16 +80,14 @@ def test_root_package_resolves_primary_facades_via_aliases(self) -> None: assert package.u is package.FlextUtilities assert {"FlextConstants", "FlextUtilities", "u"} <= set(package.__all__) - def test_model_facade_does_not_initialize_dependency_runtime(self) -> None: - """Loading model declarations must not initialize the optional DI stack.""" + def test_model_facade_does_not_import_web_runtime(self) -> None: + """Loading model declarations must not import the web framework stack.""" script = ( "import sys\n" "from flext_core import m\n" "assert m.StrictModel\n" "for name in sorted(sys.modules):\n" - " if name == 'fastapi' or name.startswith('fastapi.') " - "or name == 'dependency_injector' " - "or name.startswith('dependency_injector.'):\n" + " if name == 'fastapi' or name.startswith('fastapi.'):\n" " print(name)\n" ) diff --git a/tests/unit/test_runtime.py b/tests/unit/test_runtime.py index 6358b3beb9..a4e8f857fe 100644 --- a/tests/unit/test_runtime.py +++ b/tests/unit/test_runtime.py @@ -154,36 +154,3 @@ def test_normalize_registerable_service_passes_scalars_through(self) -> None: def test_normalize_registerable_service_rejects_unregisterable_value(self) -> None: with pytest.raises(ValueError, match="RegisterableService"): FlextRuntime.normalize_registerable_service(bytearray(b"unsupported")) - - def test_create_container_exposes_registered_object_provider(self) -> None: - container = FlextRuntime.DependencyIntegration.create_container( - services={"alpha": "beta"} - ) - - assert container.alpha() == "beta" - - def test_register_factory_with_cache_yields_singleton_instances(self) -> None: - container = FlextRuntime.DependencyIntegration.create_container() - _ = FlextRuntime.DependencyIntegration.register_factory( - container, "svc", object, cache=True - ) - - assert container.svc() is container.svc() - - def test_register_factory_without_cache_yields_distinct_instances(self) -> None: - container = FlextRuntime.DependencyIntegration.create_container() - _ = FlextRuntime.DependencyIntegration.register_factory( - container, "svc", object, cache=False - ) - - assert container.svc() is not container.svc() - - def test_register_object_rejects_duplicate_provider_name(self) -> None: - container = FlextRuntime.DependencyIntegration.create_container( - services={"alpha": "beta"} - ) - - with pytest.raises(ValueError, match="already registered"): - _ = FlextRuntime.DependencyIntegration.register_object( - container, "alpha", "other" - ) diff --git a/tests/unit/test_service_registration_spec.py b/tests/unit/test_service_registration_spec.py index f5fb380aa1..ecb5ea9737 100644 --- a/tests/unit/test_service_registration_spec.py +++ b/tests/unit/test_service_registration_spec.py @@ -1,82 +1,81 @@ -"""Characterization tests for container bootstrap registration parsing.""" +"""Behavioral tests for the container bootstrap registration spec.""" from __future__ import annotations -from typing import cast +from typing import TYPE_CHECKING import pytest from flext_tests import tm -from flext_core import u +from flext_core.container import FlextContainer +from tests import e +from tests.constants import c from tests.models import m +if TYPE_CHECKING: + from tests.protocols import p + def _factory() -> str: return "factory-value" class TestsServiceRegistrationSpecOwner: - """Behavioral ownership contract for bootstrap registration normalization.""" - - def test_utility_normalizes_raw_registration_mappings(self) -> None: - """The canonical utility converts every raw registration mapping.""" - registration = u.normalize_service_registration_spec( - m.ServiceRegistrationSpec( - services={"service": "value"}, + """The spec validates its declarations; the container registers them.""" + + def test_spec_rejects_non_mapping_services(self) -> None: + """A service collection that is not a mapping fails validation.""" + with pytest.raises(c.ValidationError): + _ = m.ServiceRegistrationSpec.model_validate({"services": ["invalid"]}) + + def test_spec_rejects_non_callable_factory(self) -> None: + """A factory declaration that is not callable fails validation.""" + with pytest.raises(c.ValidationError): + _ = m.ServiceRegistrationSpec.model_validate({ + "factories": {"factory": "not-callable"} + }) + + def test_container_registers_raw_and_prebuilt_declarations( + self, clean_container: p.Container + ) -> None: + """Raw values and prebuilt records declared by a spec both resolve.""" + container = FlextContainer( + registration=m.ServiceRegistrationSpec( + services={ + "service": "value", + "record": m.ServiceRegistration( + name="record", service="kept", service_type="str" + ), + }, factories={"factory": _factory}, - resources={"resource": _factory}, + resources={ + "resource": m.ResourceRegistration( + name="resource", factory=_factory + ) + }, ) ) - tm.that(registration.services is not None, eq=True) - services = registration.services or {} - - service_record = cast("m.ServiceRegistration", services["service"]) - assert isinstance(service_record, m.ServiceRegistration) - tm.that(service_record.name, eq="service") - tm.that(service_record.service, eq="value") - tm.that(service_record.service_type, eq="str") - tm.that(registration.factories is not None, eq=True) - factories = registration.factories or {} - factory_record = cast("m.FactoryRegistration", factories["factory"]) - assert isinstance(factory_record, m.FactoryRegistration) - tm.that(factory_record.name, eq="factory") - tm.that(factory_record.factory is _factory, eq=True) - tm.that(registration.resources is not None, eq=True) - resources = registration.resources or {} - resource_record = cast("m.ResourceRegistration", resources["resource"]) - assert isinstance(resource_record, m.ResourceRegistration) - tm.that(resource_record.name, eq="resource") - tm.that(resource_record.factory is _factory, eq=True) - - def test_utility_preserves_non_mapping_services_error(self) -> None: - """Malformed service collections retain the characterized error contract.""" - registration = m.ServiceRegistrationSpec.model_validate({ - "services": ["invalid"] - }) - - with pytest.raises(AttributeError, match="has no attribute 'items'"): - _ = u.normalize_service_registration_spec(registration) - - def test_utility_preserves_prebuilt_registration_records(self) -> None: - """Already-normalized registrations retain their object identity.""" - service = m.ServiceRegistration( - name="service", service="value", service_type="str" + tm.that(container is clean_container, eq=True) + tm.that( + sorted(container.names()), eq=["factory", "record", "resource", "service"] ) - factory = m.FactoryRegistration(name="factory", factory=_factory) - resource = m.ResourceRegistration(name="resource", factory=_factory) - - registration = u.normalize_service_registration_spec( - m.ServiceRegistrationSpec( - services={"service": service}, - factories={"factory": factory}, - resources={"resource": resource}, - ) - ) - - tm.that((registration.services or {})["service"] is service, eq=True) - tm.that((registration.factories or {})["factory"] is factory, eq=True) - tm.that((registration.resources or {})["resource"] is resource, eq=True) + tm.ok(container.resolve("service"), eq="value") + tm.ok(container.resolve("record"), eq="kept") + tm.ok(container.resolve("factory"), eq="factory-value") + tm.ok(container.resolve("resource"), eq="factory-value") + + def test_container_rejects_spec_redeclaring_a_registered_name( + self, clean_container: p.Container + ) -> None: + """Applying a spec to a container that holds its names raises.""" + spec = m.ServiceRegistrationSpec(services={"service": "value"}) + _ = clean_container.bind("service", "first") + + with pytest.raises(e.ValidationError, match="service"): + _ = FlextContainer(registration=spec) + + tm.ok(clean_container.resolve("service"), eq="first") def test_model_declares_no_registration_behavior(self) -> None: """The Pydantic model exposes only declarative schema members.""" diff --git a/uv.lock b/uv.lock index 8062840046..f64f9235f5 100644 --- a/uv.lock +++ b/uv.lock @@ -398,7 +398,7 @@ wheels = [ [[package]] name = "flext-cli" version = "0.12.0" -source = { git = "https://github.com/flext-sh/flext-cli.git?rev=0.12.0-dev#ce36f45d6438e0b90ca809715a1be6dc65cfae05" } +source = { git = "https://github.com/flext-sh/flext-cli.git?rev=0.12.0-dev#57b750a89126b6712e0ccfeeed83572248199067" } dependencies = [ { name = "annotated-types" }, { name = "beartype" }, @@ -579,7 +579,7 @@ dev = [ [[package]] name = "flext-infra" version = "0.12.0" -source = { git = "https://github.com/flext-sh/flext-infra.git?rev=0.12.0-dev#4563c04ac340487ffab47cc55e741ef6c09d7198" } +source = { git = "https://github.com/flext-sh/flext-infra.git?rev=0.12.0-dev#a4898b561147835857017a356006a1f723a39aa3" } dependencies = [ { name = "annotated-types" }, { name = "beartype" }, @@ -605,7 +605,7 @@ dependencies = [ [[package]] name = "flext-tests" version = "0.12.0" -source = { git = "https://github.com/flext-sh/flext-tests.git?rev=0.12.0-dev#7dd55ef8e8f1b7a1652e16247a1842dd22471b4e" } +source = { git = "https://github.com/flext-sh/flext-tests.git?rev=0.12.0-dev#58ba7e1d88887cbf3c8dc165a437c964e4e23677" } dependencies = [ { name = "annotated-types" }, { name = "beartype" }, @@ -1172,11 +1172,11 @@ wheels = [ [[package]] name = "nodeenv" -version = "1.10.0" +version = "1.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/105de02c1322cfada6d9710d9146ef8026419d433c9d08359a2d35805811/nodeenv-1.11.0.tar.gz", hash = "sha256:3ce8fe5b71d16e8af7039ca65257354100bc772965d6bc549070649e53b1b146", size = 99301, upload-time = "2026-09-26T11:29:21.367Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, + { url = "https://files.pythonhosted.org/packages/54/c8/12811c9b48fde162bb72b6f2e78fada9a247a09d7bb5be2050a5d099c77b/nodeenv-1.11.0-py2.py3-none-any.whl", hash = "sha256:edaa16e6c14d7cf395d75d4bbd5a26390f4dc06501a33b4e76282b02cc688a25", size = 34277, upload-time = "2026-09-26T11:29:19.933Z" }, ] [[package]] @@ -1237,11 +1237,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.11.15" +version = "4.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5f/e0/7c20b5d0e0f147c40a934e8f9e9f717bd6e3e372c4d58ca5c2fcce2b1652/platformdirs-4.11.15.tar.gz", hash = "sha256:d7419e973b2b740d428200130f80c0e79304d1c71081001db911e15b26a3a6d4", size = 46059, upload-time = "2026-09-26T02:04:58.142Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/4d/e78afe1b449720c481884ca0a2f960f85f9ffdaa34b2d127b5427422c564/platformdirs-4.12.0.tar.gz", hash = "sha256:095be5c143382b1bee917c4f3e9987a0d8d6a582261f1d061ad0c403b7695b5b", size = 58964, upload-time = "2026-09-26T15:26:00.937Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/83/246d3052d043683cc5f400ab5748bd07a99fbdf311dc771cfad7e3f9e7e0/platformdirs-4.11.15-py3-none-any.whl", hash = "sha256:84508421c77d3c6462e71dd8c0d2776c2ace0e94eb006dc4fd383bc1dcf5a176", size = 26689, upload-time = "2026-09-26T02:04:56.71Z" }, + { url = "https://files.pythonhosted.org/packages/60/3a/723ad221b7fef91ccd947f9fddbe84d38fa573e2723317aaa67bfde41015/platformdirs-4.12.0-py3-none-any.whl", hash = "sha256:f6fb2960f2f2eb0870f820f7e49e49e6c0f0589c637f34d33b260b07591327c9", size = 32965, upload-time = "2026-09-26T15:25:59.496Z" }, ] [[package]] @@ -1708,8 +1708,8 @@ wheels = [ [[package]] name = "rope" -version = "1.14.0+dc.2" -source = { git = "https://github.com/marlon-costa-dc/rope.git?rev=1.14.0%2Bdc.2#d2571e8cfabe2e00de7783e153403ac0cf4828d4" } +version = "1.14.0+dc.3" +source = { git = "https://github.com/marlon-costa-dc/rope.git?rev=1.14.0%2Bdc.3#64134bf54db1c02ca19602f320d26e1a570e7786" } dependencies = [ { name = "pytoolconfig", extra = ["global"] }, ] @@ -1769,6 +1769,7 @@ wheels = [ name = "rumdl" version = "0.2.77" source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/9e/dae5c1b451cddc6d82a9c0c5a84cc7d0a1c5087ae15dd916db89ed4f9bb2/rumdl-0.2.77.tar.gz", hash = "sha256:81a35938965a2153149c1351db13007f146149d49504e3ef81f5f1eb5656d412", size = 4878672, upload-time = "2026-09-26T14:54:23.375Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c8/c0/15494f04e84fb67a76b5889c9fefe46b96e79aac9757a9f55d2af0c31bc2/rumdl-0.2.77-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:46a86c67dbfc8d4ec7643c0696e69bd0672d4105584b55e436e43982f6e74ab5", size = 6911721, upload-time = "2026-09-23T13:25:47.365Z" }, { url = "https://files.pythonhosted.org/packages/9e/d2/23708cdeadd1b352e988a277d050478eba9504a96b55149e6531990004ea/rumdl-0.2.77-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cef5890e4995e968bf87f1d6b3ec129092204987242fe68c55781c9c90829552", size = 6449218, upload-time = "2026-09-23T13:25:41.335Z" }, From cfbb9d20b10b9fe5706c509e3458f6c510d039f8 Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Sat, 26 Sep 2026 13:49:18 -0300 Subject: [PATCH 2/6] fix(container): record class-valued services and adopt the mod and gen fixed point A class passed as a service value is recorded as a service, never mistaken for a ready registration; the type-mismatch path returns the typed failure without a cast. Examples and the architecture overview follow the truthful container. make mod removed test-suite __all__ declarations (ban-test-suite-module-all) and make gen reprojected the test inits. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/architecture/overview.md | 4 +- examples/ex_08_container_registration.py | 83 ++++++++++++------- examples/ex_08_container_scoped.py | 9 +- examples/ex_08_flext_container.expected | 10 +-- .../flextprotocolscontainer_part_01.py | 1 - src/flext_core/container.py | 35 ++++---- tests/integration/__init__.py | 13 +-- tests/integration/test_examples_execution.py | 7 -- tests/unit/__init__.py | 48 +---------- tests/unit/test_beartype_module_callables.py | 3 - tests/unit/test_constants_new.py | 3 - tests/unit/test_container_truthful.py | 8 +- tests/unit/test_coverage_loggings.py | 3 - tests/unit/test_dispatcher.py | 3 - tests/unit/test_mixins.py | 3 - tests/unit/test_registry.py | 3 - tests/unit/test_service.py | 3 - tests/unit/test_settings_validation_alias.py | 3 - .../test_utilities_collection_coverage_100.py | 3 - tests/unit/test_utilities_coverage.py | 3 - 20 files changed, 90 insertions(+), 158 deletions(-) diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index a4adf0a6a0..b60667e3aa 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -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` - (the core runtime registry: one validated write path, singleton plus scopes). + caching, data mappers, reliability helpers), and `container.py` (the core runtime + registry: one validated write path, singleton plus scopes). - **L3 – application orchestration** diff --git a/examples/ex_08_container_registration.py b/examples/ex_08_container_registration.py index be57f2a6aa..c054d8c3c5 100644 --- a/examples/ex_08_container_registration.py +++ b/examples/ex_08_container_registration.py @@ -2,12 +2,17 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from examples.constants import c from examples.protocols import p from examples.shared import ExamplesFlextShared -from flext_core import e from examples.typings import t from examples.utilities import u +from flext_core import e, r + +if TYPE_CHECKING: + from collections.abc import Callable class Ex08ContainerRegistration(ExamplesFlextShared): @@ -16,6 +21,15 @@ class Ex08ContainerRegistration(ExamplesFlextShared): _registered_service_name: str = u.PrivateAttr(default_factory=str) _registered_service_value: int = u.PrivateAttr(default_factory=int) + @staticmethod + def _write_outcome(write: Callable[[], p.Container]) -> p.Result[bool]: + """Convert one container write into a result that keeps the raised cause.""" + try: + _ = write() + except e.ValidationError as exc: + return r[bool].fail_op("container write", exc) + return r[bool].ok(True) + def _exercise_registration_and_resolution(self, container: p.Container) -> None: """Exercise register APIs plus get/get_typed/list/has checks.""" self.section("registration_and_resolution") @@ -36,27 +50,29 @@ def _exercise_registration_and_resolution(self, container: p.Container) -> None: "register.service.stored_value_matches", container.resolve(service_name, type_cls=int).unwrap() == service_value, ) - try: - _ = container.bind(service_name, self.rand_int(1, 1000)) - except e.ValidationError as exc: - self.audit_check("register.service.duplicate_raises", service_name in str(exc)) + duplicate = self._write_outcome( + lambda: container.bind(service_name, self.rand_int(1, 1000)) + ) + self.audit_check( + "register.service.duplicate_rejected", + duplicate.failure and service_name in (duplicate.error or ""), + ) self.audit_check( "register.service.duplicate_keeps_first", container.resolve(service_name, type_cls=int).unwrap() == service_value, ) - try: - _ = container.bind("", self.rand_int(1, 1000)) - except e.ValidationError as exc: - self.audit_check( - "register.service.empty_name_raises", - str(exc).endswith(c.ERR_CONTAINER_NAME_EMPTY), - ) - try: - _ = container.bind(c.ServiceName.LOGGER, self.rand_int(1, 1000)) - except e.ValidationError as exc: - self.audit_check( - "register.service.reserved_name_raises", c.ServiceName.LOGGER in str(exc) - ) + empty = self._write_outcome(lambda: container.bind("", self.rand_int(1, 1000))) + self.audit_check( + "register.service.empty_name_rejected", + c.ERR_CONTAINER_NAME_EMPTY in (empty.error or ""), + ) + reserved = self._write_outcome( + lambda: container.bind(c.ServiceName.LOGGER, self.rand_int(1, 1000)) + ) + self.audit_check( + "register.service.reserved_name_rejected", + reserved.failure and c.ServiceName.LOGGER in (reserved.error or ""), + ) factory_calls = {"count": 0} def _factory_counter() -> int: @@ -64,10 +80,14 @@ def _factory_counter() -> int: return factory_calls["count"] register_factory_ok = container.factory(factory_name, _factory_counter) - try: - _ = container.factory(factory_name, _factory_counter) - except e.ValidationError as exc: - self.audit_check("register.factory.duplicate_raises", factory_name in str(exc)) + factory_duplicate = self._write_outcome( + lambda: container.factory(factory_name, _factory_counter) + ) + self.audit_check( + "register.factory.duplicate_rejected", + factory_duplicate.failure + and factory_name in (factory_duplicate.error or ""), + ) def _factory_raises() -> int: error_message = self.rand_str(10) @@ -88,12 +108,13 @@ def _resource_data() -> t.IntMapping: return {self.rand_str(4): resource_calls["count"]} register_resource_ok = container.resource(resource_name, _resource_data) - try: - _ = container.resource(service_name, _resource_data) - except e.ValidationError as exc: - self.audit_check( - "register.resource.cross_kind_duplicate_raises", service_name in str(exc) - ) + cross_kind = self._write_outcome( + lambda: container.resource(service_name, _resource_data) + ) + self.audit_check( + "register.resource.cross_kind_duplicate_rejected", + cross_kind.failure and service_name in (cross_kind.error or ""), + ) self.audit_check( "register.resource.returns_self", register_resource_ok is container ) @@ -103,11 +124,11 @@ def _resource_data() -> t.IntMapping: get_missing = container.resolve(missing_name) get_bad_factory = container.resolve(bad_factory_name) self.audit_check("get.service.success", get_service.success) - self.audit_check("get.service.value_matches", get_service.unwrap() == service_value) - self.audit_check("get.factory.success", get_factory.success) self.audit_check( - "get.factory.invoked_per_resolve", factory_calls["count"] + "get.service.value_matches", get_service.unwrap() == service_value ) + self.audit_check("get.factory.success", get_factory.success) + self.audit_check("get.factory.invoked_per_resolve", factory_calls["count"]) self.audit_check("get.resource.success", get_resource.success) self.audit_check("get.resource.invoked_per_resolve", resource_calls["count"]) self.audit_check("get.missing.failure", get_missing.failure) diff --git a/examples/ex_08_container_scoped.py b/examples/ex_08_container_scoped.py index 6747ebfd04..eca6cd8ecd 100644 --- a/examples/ex_08_container_scoped.py +++ b/examples/ex_08_container_scoped.py @@ -8,12 +8,11 @@ from .ex_08_container_registration import Ex08ContainerRegistration + class Ex08ContainerScoped(Ex08ContainerRegistration): """Scoped container checks for the container example.""" - def _exercise_scoped( - self, container: p.ContainerLifecycle - ) -> p.ContainerLifecycle: + def _exercise_scoped(self, container: p.ContainerLifecycle) -> p.ContainerLifecycle: """Exercise scope with all supported parameter styles.""" self.section("scoped") scoped_default = container.scope() @@ -37,7 +36,9 @@ def _exercise_scoped( context=explicit_context, services={scoped_service_name: scoped_service_value}, factories={scoped_factory_name: lambda: scoped_factory_value}, - resources={scoped_resource_name: lambda: {"res": scoped_resource_value}}, + resources={ + scoped_resource_name: lambda: {"res": scoped_resource_value} + }, ), ) self.audit_check("scoped.default.new_instance", scoped_default is not container) diff --git a/examples/ex_08_flext_container.expected b/examples/ex_08_flext_container.expected index 0035ebbac4..9332f1525d 100644 --- a/examples/ex_08_flext_container.expected +++ b/examples/ex_08_flext_container.expected @@ -13,14 +13,14 @@ constants.default_max_services: 1000 register.service.returns_self: True register.service.success: True register.service.stored_value_matches: True -register.service.duplicate_raises: True +register.service.duplicate_rejected: True register.service.duplicate_keeps_first: True -register.service.empty_name_raises: True -register.service.reserved_name_raises: True -register.factory.duplicate_raises: True +register.service.empty_name_rejected: True +register.service.reserved_name_rejected: True +register.factory.duplicate_rejected: True register.factory.returns_self: True register.factory.raising_factory_registers: True -register.resource.cross_kind_duplicate_raises: True +register.resource.cross_kind_duplicate_rejected: True register.resource.returns_self: True get.service.success: True get.service.value_matches: True diff --git a/src/flext_core/_protocols/_container_parts/flextprotocolscontainer_part_01.py b/src/flext_core/_protocols/_container_parts/flextprotocolscontainer_part_01.py index 39dfd24cd0..1df704d381 100644 --- a/src/flext_core/_protocols/_container_parts/flextprotocolscontainer_part_01.py +++ b/src/flext_core/_protocols/_container_parts/flextprotocolscontainer_part_01.py @@ -8,7 +8,6 @@ from typing import TYPE_CHECKING, Protocol, runtime_checkable - if TYPE_CHECKING: from flext_core import t diff --git a/src/flext_core/container.py b/src/flext_core/container.py index 117c310556..dceac3bb52 100644 --- a/src/flext_core/container.py +++ b/src/flext_core/container.py @@ -135,11 +135,11 @@ def _service_record( name: str, value: m.ServiceRegistration | t.RegisterableService ) -> m.ServiceRegistration: """Build (or keep) the validated record of an object-backed service.""" - if isinstance(value, m.ServiceRegistration): - return value - return m.ServiceRegistration( - name=name, service=value, service_type=u.type_name(value) - ) + if isinstance(value, type) or not isinstance(value, m.ServiceRegistration): + return m.ServiceRegistration( + name=name, service=value, service_type=u.type_name(value) + ) + return value @staticmethod def _factory_record( @@ -259,13 +259,10 @@ def resolve[T: t.RegisterableService]( return result if self._matches_service_type(result.value, type_cls): return r[T].ok(result.value) - return cast( - "p.Result[T]", - r[T].from_result( - e.fail_type_mismatch( - type_cls.__name__, type(result.value).__name__, result_type=r[T] - ) - ), + return r[T].from_result( + e.fail_type_mismatch( + type_cls.__name__, type(result.value).__name__, result_type=r[T] + ) ) @override @@ -321,8 +318,13 @@ def initialize_registrations( @override def register_core_services(self) -> None: """Register the reserved core services that are not registered yet.""" - core: tuple[tuple[str, Callable[[], m.ServiceRegistration | m.FactoryRegistration]], ...] = ( - (c.Directory.CONFIG, partial(self._service_record, c.Directory.CONFIG, self._config)), + core: tuple[ + tuple[str, Callable[[], m.ServiceRegistration | m.FactoryRegistration]], ... + ] = ( + ( + c.Directory.CONFIG, + partial(self._service_record, c.Directory.CONFIG, self._config), + ), ( c.ServiceName.LOGGER, partial( @@ -331,7 +333,10 @@ def register_core_services(self) -> None: partial(u.fetch_logger, c.LOGGER_NAME_FLEXT_CORE), ), ), - (c.FIELD_CONTEXT, partial(self._service_record, c.FIELD_CONTEXT, self._context)), + ( + c.FIELD_CONTEXT, + partial(self._service_record, c.FIELD_CONTEXT, self._context), + ), ( c.ServiceName.COMMAND_BUS, lambda: self._service_record( diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index f5d6c7f616..92a8c75413 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -4,23 +4,14 @@ from __future__ import annotations from types import MappingProxyType -from typing import TYPE_CHECKING from flext_core.lazy import build_lazy_import_map, install_lazy_exports -if TYPE_CHECKING: - from .test_examples_execution import TestsFlextExamplesExecution - - -__all__: tuple[str, ...] = ("TestsFlextExamplesExecution",) +__all__: tuple[str, ...] = () _LAZY_IMPORTS = MappingProxyType( build_lazy_import_map( - MappingProxyType({ - ".test_examples_execution": ("TestsFlextExamplesExecution",) - }), - alias_groups=MappingProxyType({}), - sort_keys=False, + MappingProxyType({}), alias_groups=MappingProxyType({}), sort_keys=False ) ) diff --git a/tests/integration/test_examples_execution.py b/tests/integration/test_examples_execution.py index ba8148074e..b1cf3d32e3 100644 --- a/tests/integration/test_examples_execution.py +++ b/tests/integration/test_examples_execution.py @@ -13,16 +13,12 @@ import re import sys from pathlib import Path -from typing import TYPE_CHECKING import pytest from flext_tests import tm, u from tests.constants import c -if TYPE_CHECKING: - from tests.typings import t - _CHECK_COUNT_RE = re.compile(r"\((\d+) checks\)") @@ -77,6 +73,3 @@ def test_public_example_scripts_match_golden_files( tm.that(expected_path.exists(), eq=True) tm.that(actual_path.exists(), eq=False) - - -__all__: t.MutableSequenceOf[str] = ["TestsFlextExamplesExecution"] diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index b1420c2a3e..c97339931f 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -10,57 +10,13 @@ if TYPE_CHECKING: from . import _models, _utilities - from .test_beartype_module_callables import TestsFlextCoreBeartypeModuleCallables - from .test_constants_new import TestsFlextConstantsNew - from .test_coverage_loggings import TestsFlextCoverageLoggings - from .test_dispatcher import TestsFlextCoreDispatcher - from .test_mixins import TestsFlextMixins - from .test_registry import TestsFlextCoreRegistry - from .test_service import TestsFlextService - from .test_settings_validation_alias import TestsFlextCoreSettingsValidationAlias - from .test_utilities_collection_coverage_100 import ( - TestsFlextCoreUtilitiesCollection, - ) - from .test_utilities_coverage import TestsFlextCoreUtilitiesCoverage -__all__: tuple[str, ...] = ( - "TestsFlextConstantsNew", - "TestsFlextCoreBeartypeModuleCallables", - "TestsFlextCoreDispatcher", - "TestsFlextCoreRegistry", - "TestsFlextCoreSettingsValidationAlias", - "TestsFlextCoreUtilitiesCollection", - "TestsFlextCoreUtilitiesCoverage", - "TestsFlextCoverageLoggings", - "TestsFlextMixins", - "TestsFlextService", - "_models", - "_utilities", -) +__all__: tuple[str, ...] = ("_models", "_utilities") _LAZY_IMPORTS = MappingProxyType( build_lazy_import_map( - MappingProxyType({ - "._models": ("_models",), - "._utilities": ("_utilities",), - ".test_beartype_module_callables": ( - "TestsFlextCoreBeartypeModuleCallables", - ), - ".test_constants_new": ("TestsFlextConstantsNew",), - ".test_coverage_loggings": ("TestsFlextCoverageLoggings",), - ".test_dispatcher": ("TestsFlextCoreDispatcher",), - ".test_mixins": ("TestsFlextMixins",), - ".test_registry": ("TestsFlextCoreRegistry",), - ".test_service": ("TestsFlextService",), - ".test_settings_validation_alias": ( - "TestsFlextCoreSettingsValidationAlias", - ), - ".test_utilities_collection_coverage_100": ( - "TestsFlextCoreUtilitiesCollection", - ), - ".test_utilities_coverage": ("TestsFlextCoreUtilitiesCoverage",), - }), + MappingProxyType({"._models": ("_models",), "._utilities": ("_utilities",)}), alias_groups=MappingProxyType({}), sort_keys=False, ) diff --git a/tests/unit/test_beartype_module_callables.py b/tests/unit/test_beartype_module_callables.py index 1b0825030e..0b7985ef96 100644 --- a/tests/unit/test_beartype_module_callables.py +++ b/tests/unit/test_beartype_module_callables.py @@ -97,6 +97,3 @@ def test_the_proxy_really_would_raise_on_resolution(self) -> None: with pytest.raises(RuntimeError, match=_OUTSIDE_CONTEXT): proxy.__class__ = object - - -__all__: list[str] = ["TestsFlextCoreBeartypeModuleCallables"] diff --git a/tests/unit/test_constants_new.py b/tests/unit/test_constants_new.py index 5f9fdafaf4..5920282787 100644 --- a/tests/unit/test_constants_new.py +++ b/tests/unit/test_constants_new.py @@ -191,6 +191,3 @@ def test_ldap_dn_pattern_rejects_adversarial_component_chain(self) -> None: """LDAP DN validation rejects the CodeQL adversarial shape.""" adversarial = "A=+" + ",A=+ " * 256 + ",A=" tm.that(bool(c.PATTERN_LDAP_DN_RE.fullmatch(adversarial)), eq=False) - - -__all__: list[str] = ["TestsFlextConstantsNew"] diff --git a/tests/unit/test_container_truthful.py b/tests/unit/test_container_truthful.py index d2a79d6895..08e7102345 100644 --- a/tests/unit/test_container_truthful.py +++ b/tests/unit/test_container_truthful.py @@ -97,12 +97,8 @@ def test_scope_binds_core_services_to_its_own_runtime( """The scoped settings and context services are the scope's own.""" scoped = clean_container.scope(subproject="unit") - tm.that( - tm.ok(scoped.resolve(c.Directory.CONFIG)) is scoped.settings, eq=True - ) - tm.that( - tm.ok(scoped.resolve(c.FIELD_CONTEXT)) is scoped.context, eq=True - ) + tm.that(tm.ok(scoped.resolve(c.Directory.CONFIG)) is scoped.settings, eq=True) + tm.that(tm.ok(scoped.resolve(c.FIELD_CONTEXT)) is scoped.context, eq=True) tm.that(scoped.settings is clean_container.settings, eq=False) def test_scope_spec_overrides_inherited_registration( diff --git a/tests/unit/test_coverage_loggings.py b/tests/unit/test_coverage_loggings.py index 8bf9807a9f..909a5da35b 100644 --- a/tests/unit/test_coverage_loggings.py +++ b/tests/unit/test_coverage_loggings.py @@ -129,6 +129,3 @@ def test_logging_with_large_context_emits_context_tokens(self) -> None: ) tm.that(result.value, eq=True) - - -__all__: list[str] = ["TestsFlextCoverageLoggings"] diff --git a/tests/unit/test_dispatcher.py b/tests/unit/test_dispatcher.py index c098a64b5a..ba7176f006 100644 --- a/tests/unit/test_dispatcher.py +++ b/tests/unit/test_dispatcher.py @@ -240,9 +240,6 @@ def test_dispatch_logs_handler_exception_type_and_message(self) -> None: assert failure_detail in emitted -__all__: list[str] = ["TestsFlextCoreDispatcher"] - - RouteMessage = TestsFlextCoreDispatcher.RouteMessage RecordingHandler = TestsFlextCoreDispatcher.RecordingHandler FailingHandler = TestsFlextCoreDispatcher.FailingHandler diff --git a/tests/unit/test_mixins.py b/tests/unit/test_mixins.py index 6cc2d4ae9f..ba7191ffe9 100644 --- a/tests/unit/test_mixins.py +++ b/tests/unit/test_mixins.py @@ -141,6 +141,3 @@ def test_correlation_id_round_trips_through_flext_context(self) -> None: FlextContext.apply_correlation_id("trace-42") assert FlextContext.resolve_correlation_id() == "trace-42" - - -__all__: t.MutableSequenceOf[str] = ["TestsFlextMixins"] diff --git a/tests/unit/test_registry.py b/tests/unit/test_registry.py index a80a056ac7..2a0628a83a 100644 --- a/tests/unit/test_registry.py +++ b/tests/unit/test_registry.py @@ -146,6 +146,3 @@ def test_summary_success_reflects_error_state( assert summary.success is expected_success assert summary.failure is (not expected_success) assert len(summary.registered) == 1 - - -__all__: t.MutableSequenceOf[str] = ["TestsFlextCoreRegistry"] diff --git a/tests/unit/test_service.py b/tests/unit/test_service.py index 3aae73bf38..807e85dbcf 100644 --- a/tests/unit/test_service.py +++ b/tests/unit/test_service.py @@ -162,6 +162,3 @@ def test_track_yields_named_operation_metrics(self) -> None: with service.track("load_users") as metrics: assert isinstance(metrics, Mapping) assert metrics["operation_name"] == "load_users" - - -__all__: list[str] = ["TestsFlextService"] diff --git a/tests/unit/test_settings_validation_alias.py b/tests/unit/test_settings_validation_alias.py index fc44710f61..f5ef77dbf4 100644 --- a/tests/unit/test_settings_validation_alias.py +++ b/tests/unit/test_settings_validation_alias.py @@ -155,6 +155,3 @@ def test_unknown_override_key_raises_value_error(self) -> None: TestsFlextCoreSettingsValidationAlias._AliasFieldSettings.update_global( not_a_field="x" ) - - -__all__: t.MutableSequenceOf[str] = ["TestsFlextCoreSettingsValidationAlias"] diff --git a/tests/unit/test_utilities_collection_coverage_100.py b/tests/unit/test_utilities_collection_coverage_100.py index 33feab9243..dcfe09551a 100644 --- a/tests/unit/test_utilities_collection_coverage_100.py +++ b/tests/unit/test_utilities_collection_coverage_100.py @@ -245,6 +245,3 @@ def test_merge_mappings_override_replaces_values(self) -> None: tm.that(result.value["a"], eq=1) tm.that(result.value["c"], eq=3) tm.that(result.value["b"], is_=dict) - - -__all__: list[str] = ["TestsFlextCoreUtilitiesCollection"] diff --git a/tests/unit/test_utilities_coverage.py b/tests/unit/test_utilities_coverage.py index ff113be280..bdcca9e73a 100644 --- a/tests/unit/test_utilities_coverage.py +++ b/tests/unit/test_utilities_coverage.py @@ -192,6 +192,3 @@ def test_safe_string_rejects_empty_input(self, text: str | None) -> None: """safe_string raises ValueError for None, empty, or whitespace input.""" with pytest.raises(ValueError, match="Text"): u.safe_string(text) - - -__all__: list[str] = ["TestsFlextCoreUtilitiesCoverage"] From 51d2f42c18cfe9b416d07ad22f9942144f3c1d32 Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Sat, 26 Sep 2026 14:09:01 -0300 Subject: [PATCH 3/6] fix(container): keep the typed narrowing of an already-validated service record A class object is never an instance of the ServiceRegistration model, so the extra isinstance(value, type) branch changed no behavior and broke mypy's narrowing (return-value: ServiceRegistration | type[object]). Caught by the local pre-push gate. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/flext_core/container.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/flext_core/container.py b/src/flext_core/container.py index dceac3bb52..1f343dd3bc 100644 --- a/src/flext_core/container.py +++ b/src/flext_core/container.py @@ -135,11 +135,11 @@ def _service_record( name: str, value: m.ServiceRegistration | t.RegisterableService ) -> m.ServiceRegistration: """Build (or keep) the validated record of an object-backed service.""" - if isinstance(value, type) or not isinstance(value, m.ServiceRegistration): - return m.ServiceRegistration( - name=name, service=value, service_type=u.type_name(value) - ) - return value + if isinstance(value, m.ServiceRegistration): + return value + return m.ServiceRegistration( + name=name, service=value, service_type=u.type_name(value) + ) @staticmethod def _factory_record( From 7f2fa22f24b7d6959364ce9e60f5232941bfde09 Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Sat, 26 Sep 2026 14:16:13 -0300 Subject: [PATCH 4/6] fix(container): exclude class objects from the validated-record branch t.RegisterableService admits type[object], and mypy keeps type[object] after isinstance(value, ServiceRegistration) because a class may be an instance through its metaclass. Excluding classes in the same condition keeps one construction path and gives mypy the exact narrowing. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/flext_core/container.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/flext_core/container.py b/src/flext_core/container.py index 1f343dd3bc..57fb1737f5 100644 --- a/src/flext_core/container.py +++ b/src/flext_core/container.py @@ -135,7 +135,9 @@ def _service_record( name: str, value: m.ServiceRegistration | t.RegisterableService ) -> m.ServiceRegistration: """Build (or keep) the validated record of an object-backed service.""" - if isinstance(value, m.ServiceRegistration): + # A class object (RegisterableService admits type[object]) is a service + # to record, never an already-validated record. + if isinstance(value, m.ServiceRegistration) and not isinstance(value, type): return value return m.ServiceRegistration( name=name, service=value, service_type=u.type_name(value) From ef0b5cd15697daf311bb995b434d9937c6fe5f1a Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Sat, 26 Sep 2026 14:22:23 -0300 Subject: [PATCH 5/6] refactor(container): exterminate the registration-record helpers _service_record, _factory_record and _resource_record only built Pydantic models and accepted either a raw value or a prebuilt record (m.X | t.Y) sorted out by isinstance. The bootstrap spec now declares raw values only (t.RegisterableService, t.FactoryCallable, t.ResourceCallable), the container builds each record with the model itself, the spec delegates to bind/factory/resource, and service_type is a computed field derived from the service. The spec test asserts raw declarations resolve and a prebuilt record is rejected at the boundary. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../flextmodelscontainer_part_01.py | 11 ++- .../flextmodelscontainer_part_03.py | 15 +--- src/flext_core/container.py | 75 +++++++------------ tests/unit/test_service_registration_spec.py | 31 ++++---- 4 files changed, 53 insertions(+), 79 deletions(-) diff --git a/src/flext_core/_models/_container_parts/flextmodelscontainer_part_01.py b/src/flext_core/_models/_container_parts/flextmodelscontainer_part_01.py index 733121a9f8..46f31160d9 100644 --- a/src/flext_core/_models/_container_parts/flextmodelscontainer_part_01.py +++ b/src/flext_core/_models/_container_parts/flextmodelscontainer_part_01.py @@ -19,6 +19,7 @@ from ..._runtime._container import FlextRuntimeContainer as FlextRuntime from ..._utilities.generators import FlextUtilitiesGenerators as ug +from ..._utilities.guards_type_core import FlextUtilitiesGuardsTypeCore as ug_type from ..._utilities.pydantic import FlextUtilitiesPydantic as up from ..base import FlextModelsBase as m from ..containers import FlextModelsContainers @@ -61,14 +62,16 @@ class ServiceRegistration(m.ArbitraryTypesModel): None, description="Additional service metadata (JSON-serializable)" ), ] = None - service_type: Annotated[ - str | None, - mp.Field(None, description="Service type name (e.g., 'DatabaseService')"), - ] = None tags: Annotated[ t.StrSequence, mp.Field(description="Service tags for categorization") ] = mp.Field(default_factory=tuple) + @mp.computed_field + @property + def service_type(self) -> str: + """Type name of the registered service, derived from the service.""" + return ug_type.type_name(self.service) + @up.field_validator("service", mode="before") @classmethod def validate_service( diff --git a/src/flext_core/_models/_container_parts/flextmodelscontainer_part_03.py b/src/flext_core/_models/_container_parts/flextmodelscontainer_part_03.py index 230d9f77cf..96c0b23049 100644 --- a/src/flext_core/_models/_container_parts/flextmodelscontainer_part_03.py +++ b/src/flext_core/_models/_container_parts/flextmodelscontainer_part_03.py @@ -53,10 +53,7 @@ class ServiceRegistrationSpec(m.ArbitraryTypesModel): ), ] = None services: Annotated[ - t.MappingKV[ - str, FlextModelsContainer.ServiceRegistration | t.RegisterableService - ] - | None, + t.MappingKV[str, t.RegisterableService] | None, mp.Field( None, title="Services", @@ -65,10 +62,7 @@ class ServiceRegistrationSpec(m.ArbitraryTypesModel): ), ] = None factories: Annotated[ - t.MappingKV[ - str, FlextModelsContainer.FactoryRegistration | t.FactoryCallable - ] - | None, + t.MappingKV[str, t.FactoryCallable] | None, mp.Field( None, title="Factories", @@ -77,10 +71,7 @@ class ServiceRegistrationSpec(m.ArbitraryTypesModel): ), ] = None resources: Annotated[ - t.MappingKV[ - str, FlextModelsContainer.ResourceRegistration | t.ResourceCallable - ] - | None, + t.MappingKV[str, t.ResourceCallable] | None, mp.Field( None, title="Resources", diff --git a/src/flext_core/container.py b/src/flext_core/container.py index 57fb1737f5..de4137ce16 100644 --- a/src/flext_core/container.py +++ b/src/flext_core/container.py @@ -130,37 +130,6 @@ def _matches_service_type[T: t.RegisterableService]( """Narrow a resolved service through its structural runtime type.""" return isinstance(value, expected) - @staticmethod - def _service_record( - name: str, value: m.ServiceRegistration | t.RegisterableService - ) -> m.ServiceRegistration: - """Build (or keep) the validated record of an object-backed service.""" - # A class object (RegisterableService admits type[object]) is a service - # to record, never an already-validated record. - if isinstance(value, m.ServiceRegistration) and not isinstance(value, type): - return value - return m.ServiceRegistration( - name=name, service=value, service_type=u.type_name(value) - ) - - @staticmethod - def _factory_record( - name: str, value: m.FactoryRegistration | t.FactoryCallable - ) -> m.FactoryRegistration: - """Build (or keep) the validated record of a factory.""" - if isinstance(value, m.FactoryRegistration): - return value - return m.FactoryRegistration(name=name, factory=value) - - @staticmethod - def _resource_record( - name: str, value: m.ResourceRegistration | t.ResourceCallable - ) -> m.ResourceRegistration: - """Build (or keep) the validated record of a resource.""" - if isinstance(value, m.ResourceRegistration): - return value - return m.ResourceRegistration(name=name, factory=value) - def _write( self, name: str, @@ -195,26 +164,32 @@ def _write( def _write_spec(self, spec: m.ServiceRegistrationSpec) -> None: """Write every service, factory and resource declared by a spec.""" for name, service in (spec.services or {}).items(): - _ = self._write(name, partial(self._service_record, name, service)) + _ = self.bind(name, service) for name, factory in (spec.factories or {}).items(): - _ = self._write(name, partial(self._factory_record, name, factory)) + _ = self.factory(name, factory) for name, resource in (spec.resources or {}).items(): - _ = self._write(name, partial(self._resource_record, name, resource)) + _ = self.resource(name, resource) @override def bind(self, name: str, impl: t.RegisterableService) -> Self: """Bind a concrete service instance or value.""" - return self._write(name, partial(self._service_record, name, impl)) + return self._write( + name, partial(m.ServiceRegistration, name=name, service=impl) + ) @override def factory(self, name: str, impl: t.FactoryCallable) -> Self: """Bind a factory callable invoked on every resolve.""" - return self._write(name, partial(self._factory_record, name, impl)) + return self._write( + name, partial(m.FactoryRegistration, name=name, factory=impl) + ) @override def resource(self, name: str, impl: t.ResourceCallable) -> Self: """Bind a resource factory invoked on every resolve.""" - return self._write(name, partial(self._resource_record, name, impl)) + return self._write( + name, partial(m.ResourceRegistration, name=name, factory=impl) + ) def _resolve_callable( self, callable_obj: t.FactoryCallable, kind: str @@ -325,24 +300,28 @@ def register_core_services(self) -> None: ] = ( ( c.Directory.CONFIG, - partial(self._service_record, c.Directory.CONFIG, self._config), + partial( + m.ServiceRegistration, name=c.Directory.CONFIG, service=self._config + ), ), ( c.ServiceName.LOGGER, partial( - self._factory_record, - c.ServiceName.LOGGER, - partial(u.fetch_logger, c.LOGGER_NAME_FLEXT_CORE), + m.FactoryRegistration, + name=c.ServiceName.LOGGER, + factory=partial(u.fetch_logger, c.LOGGER_NAME_FLEXT_CORE), ), ), ( c.FIELD_CONTEXT, - partial(self._service_record, c.FIELD_CONTEXT, self._context), + partial( + m.ServiceRegistration, name=c.FIELD_CONTEXT, service=self._context + ), ), ( c.ServiceName.COMMAND_BUS, - lambda: self._service_record( - c.ServiceName.COMMAND_BUS, u.build_dispatcher() + lambda: m.ServiceRegistration( + name=c.ServiceName.COMMAND_BUS, service=u.build_dispatcher() ), ), ) @@ -487,14 +466,18 @@ def _apply_explicit_bootstrap( self._config = registration.settings _ = self._write( c.Directory.CONFIG, - partial(self._service_record, c.Directory.CONFIG, self._config), + partial( + m.ServiceRegistration, name=c.Directory.CONFIG, service=self._config + ), internal=True, ) if registration.context is not None: self._context = registration.context _ = self._write( c.FIELD_CONTEXT, - partial(self._service_record, c.FIELD_CONTEXT, self._context), + partial( + m.ServiceRegistration, name=c.FIELD_CONTEXT, service=self._context + ), internal=True, ) diff --git a/tests/unit/test_service_registration_spec.py b/tests/unit/test_service_registration_spec.py index ecb5ea9737..7db44373ef 100644 --- a/tests/unit/test_service_registration_spec.py +++ b/tests/unit/test_service_registration_spec.py @@ -35,36 +35,33 @@ def test_spec_rejects_non_callable_factory(self) -> None: "factories": {"factory": "not-callable"} }) - def test_container_registers_raw_and_prebuilt_declarations( + def test_container_registers_the_declared_raw_values( self, clean_container: p.Container ) -> None: - """Raw values and prebuilt records declared by a spec both resolve.""" + """Services, factories and resources declared by a spec all resolve.""" container = FlextContainer( registration=m.ServiceRegistrationSpec( - services={ - "service": "value", - "record": m.ServiceRegistration( - name="record", service="kept", service_type="str" - ), - }, + services={"service": "value"}, factories={"factory": _factory}, - resources={ - "resource": m.ResourceRegistration( - name="resource", factory=_factory - ) - }, + resources={"resource": _factory}, ) ) tm.that(container is clean_container, eq=True) - tm.that( - sorted(container.names()), eq=["factory", "record", "resource", "service"] - ) + tm.that(sorted(container.names()), eq=["factory", "resource", "service"]) tm.ok(container.resolve("service"), eq="value") - tm.ok(container.resolve("record"), eq="kept") tm.ok(container.resolve("factory"), eq="factory-value") tm.ok(container.resolve("resource"), eq="factory-value") + def test_spec_rejects_a_prebuilt_record_as_a_factory(self) -> None: + """A spec declares raw values only; a registration record is not one.""" + record = m.FactoryRegistration(name="factory", factory=_factory) + + with pytest.raises(c.ValidationError): + _ = m.ServiceRegistrationSpec.model_validate({ + "factories": {"factory": record} + }) + def test_container_rejects_spec_redeclaring_a_registered_name( self, clean_container: p.Container ) -> None: From f8726a53a3b8c2f1a4d4e288618ac974d851243a Mon Sep 17 00:00:00 2001 From: Marlon Costa Date: Sat, 26 Sep 2026 14:32:43 -0300 Subject: [PATCH 6/6] fix(container): scope inherits the registered values, never the records scope() handed the parent's registration records to the bootstrap spec: factories and resources failed the raw-callable contract, and a service record was silently registered as the service value itself. The scope now passes record.service and record.factory, the one declared type. Runtime: examples.ex_08_flext_container passes its 77 checks. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/flext_core/container.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/flext_core/container.py b/src/flext_core/container.py index de4137ce16..3b12b39219 100644 --- a/src/flext_core/container.py +++ b/src/flext_core/container.py @@ -354,7 +354,7 @@ def scope( context=scoped_context, services={ **{ - name: record + name: record.service for name, record in inherited.items() if isinstance(record, m.ServiceRegistration) }, @@ -362,7 +362,7 @@ def scope( }, factories={ **{ - name: record + name: record.factory for name, record in inherited.items() if isinstance(record, m.FactoryRegistration) }, @@ -370,7 +370,7 @@ def scope( }, resources={ **{ - name: record + name: record.factory for name, record in inherited.items() if isinstance(record, m.ResourceRegistration) },