From 61016d2245bfeabd118c886f192fdf314e12c3f4 Mon Sep 17 00:00:00 2001 From: ancongui Date: Wed, 10 Jun 2026 15:43:07 +0200 Subject: [PATCH 1/3] fix(container): do not autowire Any-typed params; fall back to default An __init__ param typed Any (e.g. 'registry: Any = None') hit the plain-class fast path and called self.resolve(Any), matching whatever bean was registered under Any (e.g. an '@bean -> Any' cache health indicator) and injecting the wrong object. Treat Any like 'type': raise NoSuchBeanError so the caller uses the parameter default. --- src/pyfly/container/container.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pyfly/container/container.py b/src/pyfly/container/container.py index 7b5e566..64e0986 100644 --- a/src/pyfly/container/container.py +++ b/src/pyfly/container/container.py @@ -543,7 +543,12 @@ def _resolve_param(self, param_type: type) -> Any: # Fast path: a plain class dependency — no typing origin and not a PEP 604 union. if origin is None and not isinstance(param_type, types.UnionType): - if param_type is type: + # `type` and `Any` are not injectable dependencies — let the caller fall back + # to the parameter's default (NoSuchBeanError + has_default => use default). + # `Any` must NOT be resolved: it would match whatever bean happens to be + # registered under `Any` (e.g. an `@bean ... -> Any`), injecting the wrong + # object — e.g. a CacheHealthIndicator landing in `registry: Any = None`. + if param_type is type or param_type is Any: raise NoSuchBeanError(bean_type=None) return self.resolve(param_type) From e0448904ea80b41372e1e38584c20aa170cb239c Mon Sep 17 00:00:00 2001 From: ancongui Date: Wed, 10 Jun 2026 15:53:02 +0200 Subject: [PATCH 2/3] fix(container): Optional[Any] (Any | None) is not injectable either Same root cause as the Any fast-path: a param typed 'Any | None' (e.g. a @bean method's 'metrics: Any | None = None') reduced to resolve(Any) in the Optional branch, injecting whatever bean was registered under Any (a CacheHealthIndicator) into RuleEngineService.metrics -> 'has no attribute counter'. Return None for Optional[Any] so the parameter default is used. --- src/pyfly/container/container.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pyfly/container/container.py b/src/pyfly/container/container.py index 64e0986..3b1692c 100644 --- a/src/pyfly/container/container.py +++ b/src/pyfly/container/container.py @@ -570,6 +570,11 @@ def _resolve_param(self, param_type: type) -> Any: args = get_args(param_type) non_none = [a for a in args if a is not type(None)] if len(non_none) == 1: + # Optional[Any] (`Any | None`) is not an injectable dependency — `Any` + # would match whatever bean happens to be registered under `Any`, + # injecting the wrong object. Leave it unset (None). + if non_none[0] is Any: + return None try: return self.resolve(non_none[0]) except (NoSuchBeanError, NoUniqueBeanError): From 70c46fe3b7d7d0789a4916625a8062b4095012f8 Mon Sep 17 00:00:00 2001 From: ancongui Date: Wed, 10 Jun 2026 15:57:10 +0200 Subject: [PATCH 3/3] chore: release artifacts v26.06.96 (changelog, badge, version, test) --- CHANGELOG.md | 15 ++++++++++++++ README.md | 2 +- pyproject.toml | 2 +- tests/container/test_any_not_injectable.py | 24 ++++++++++++++++++++++ uv.lock | 2 +- 5 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 tests/container/test_any_not_injectable.py diff --git a/CHANGELOG.md b/CHANGELOG.md index dba4b8c..3367ab1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). --- +## v26.06.96 (2026-06-10) + +### Fixed + +- **`Any` / `Any | None` parameters are no longer autowired.** A constructor or + `@bean`-method parameter typed `Any` (e.g. `registry: Any = None`) or `Any | None` + (e.g. `metrics: Any | None = None`) was resolved against the container, matching + whatever bean was registered under `Any` (such as an `@bean ... -> Any` cache health + indicator). That injected the wrong object, e.g. `'CacheHealthIndicator' object has + no attribute 'counter'` landing in `RuleEngineService(metrics=...)`. `Any` is now + treated like `type` — not injectable — so the parameter falls back to its default. + Regression tests in `tests/container/test_any_not_injectable.py`. + +--- + ## v26.06.95 (2026-06-10) ### Fixed diff --git a/README.md b/README.md index 0c4c9de..ff106c3 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Firefly Framework Python 3.12+ License: Apache 2.0 - Version: 26.06.95 + Version: 26.06.96 Type Checked: mypy strict Code Style: Ruff Async First diff --git a/pyproject.toml b/pyproject.toml index 17b6ee5..d949537 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "pyfly" # CalVer YY.MM.PATCH — package metadata uses PEP 440 normalized form (26.5.4); # git tag, GitHub release and human-readable display use leading-zero form # (v26.05.04) to match the Java/.NET/Go siblings. -version = "26.6.95" +version = "26.6.96" description = "The official Python implementation of the Firefly Framework — DI, CQRS, EDA, hexagonal architecture, and more." readme = "README.md" license = "Apache-2.0" diff --git a/tests/container/test_any_not_injectable.py b/tests/container/test_any_not_injectable.py new file mode 100644 index 0000000..0b87803 --- /dev/null +++ b/tests/container/test_any_not_injectable.py @@ -0,0 +1,24 @@ +"""Regression: ``Any`` / ``Any | None`` parameters are NOT injectable. + +A parameter typed ``Any`` (or ``Any | None``) must fall back to its default +instead of resolving to whatever bean happens to be registered under ``Any`` +(e.g. an ``@bean ... -> Any``). Injecting the wrong object caused +`'CacheHealthIndicator' object has no attribute 'counter'` at startup when such a +value landed in ``RuleEngineService(metrics: Any | None = None)``. +""" + +from typing import Any + +import pytest + +from pyfly.container.container import Container +from pyfly.container.exceptions import NoSuchBeanError + + +def test_plain_any_is_not_injectable() -> None: + with pytest.raises(NoSuchBeanError): + Container()._resolve_param(Any) + + +def test_optional_any_resolves_to_none() -> None: + assert Container()._resolve_param(Any | None) is None diff --git a/uv.lock b/uv.lock index 90fbc76..d57ed1b 100644 --- a/uv.lock +++ b/uv.lock @@ -2160,7 +2160,7 @@ wheels = [ [[package]] name = "pyfly" -version = "26.6.95" +version = "26.6.96" source = { editable = "." } dependencies = [ { name = "pydantic" },