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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<a href="https://github.com/fireflyframework"><img src="https://img.shields.io/badge/Firefly_Framework-official-ff6600?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xMiAyQzYuNDggMiAyIDYuNDggMiAxMnM0LjQ4IDEwIDEwIDEwIDEwLTQuNDggMTAtMTBTMTcuNTIgMiAxMiAyeiIvPjwvc3ZnPg==" alt="Firefly Framework"></a>
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.12%2B-blue?logo=python&logoColor=white" alt="Python 3.12+"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-green" alt="License: Apache 2.0"></a>
<a href="#"><img src="https://img.shields.io/badge/version-26.06.95-brightgreen" alt="Version: 26.06.95"></a>
<a href="#"><img src="https://img.shields.io/badge/version-26.06.96-brightgreen" alt="Version: 26.06.96"></a>
<a href="#"><img src="https://img.shields.io/badge/type--checked-mypy%20strict-blue?logo=python&logoColor=white" alt="Type Checked: mypy strict"></a>
<a href="#"><img src="https://img.shields.io/badge/code%20style-ruff-purple?logo=ruff&logoColor=white" alt="Code Style: Ruff"></a>
<a href="#"><img src="https://img.shields.io/badge/async-first-brightgreen" alt="Async First"></a>
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
12 changes: 11 additions & 1 deletion src/pyfly/container/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -565,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):
Expand Down
24 changes: 24 additions & 0 deletions tests/container/test_any_not_injectable.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading