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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

---

## v26.06.09 (2026-06-05)

### Fixed

- **Startup `wiring_summary` now reports EDA event listeners.** The bean/wiring
summary logged at startup surfaced `event_listeners` (`@app_event_listener`),
`message_listeners`, `cqrs_handlers`, `scheduled_tasks`, `async_methods`, and
`post_processors` — but **omitted EDA `@event_listener` subscriptions**
(tracked under `event_listeners_eda`). So a service that wired EDA listeners saw
them reported as absent in the summary, which is misleading when using the
summary to diagnose noop-wiring (as `debugging-async-services` recommends). The
EDA listeners were correctly subscribed and dispatched — only the summary line
under-reported them. The summary now includes `event_listeners_eda`; the field
assembly was extracted to a testable `pyfly.core.application._wiring_summary_fields`
helper with regression tests. No behavioural change to event subscription or
dispatch. Found while validating the `implement-eda` skill (the skill and the
eda/messaging runtime were already correct).

---

## v26.06.08 (2026-06-05)

### 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.08-brightgreen" alt="Version: 26.06.08"></a>
<a href="#"><img src="https://img.shields.io/badge/version-26.06.09-brightgreen" alt="Version: 26.06.09"></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.8"
version = "26.6.9"
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
2 changes: 1 addition & 1 deletion src/pyfly/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
# limitations under the License.
"""PyFly — Enterprise Python Framework."""

__version__ = "26.06.08"
__version__ = "26.06.09"
28 changes: 19 additions & 9 deletions src/pyfly/core/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,24 @@ def decorator(cls: type[T]) -> type[T]:
return decorator


def _wiring_summary_fields(wiring: dict[str, int]) -> dict[str, int]:
"""Build the fields shown in the startup ``wiring_summary`` log line.

Surfaces every decorator-wiring count — including EDA ``@event_listener``
subscriptions (``event_listeners_eda``), which were previously omitted so the
summary under-reported wired EDA listeners as absent.
"""
return {
"event_listeners": wiring.get("event_listeners", 0),
"event_listeners_eda": wiring.get("event_listeners_eda", 0),
"message_listeners": wiring.get("message_listeners", 0),
"cqrs_handlers": wiring.get("cqrs_handlers", 0),
"scheduled_tasks": wiring.get("scheduled", 0),
"async_methods": wiring.get("async_methods", 0),
"post_processors": wiring.get("post_processors", 0),
}


class PyFlyApplication:
"""Main application class that bootstraps the framework.

Expand Down Expand Up @@ -244,15 +262,7 @@ def _log_startup_summary(self) -> None:
# Decorator wiring counts
wiring = self._context.wiring_counts
if any(wiring.values()):
self._logger.info(
"wiring_summary",
event_listeners=wiring.get("event_listeners", 0),
message_listeners=wiring.get("message_listeners", 0),
cqrs_handlers=wiring.get("cqrs_handlers", 0),
scheduled_tasks=wiring.get("scheduled", 0),
async_methods=wiring.get("async_methods", 0),
post_processors=wiring.get("post_processors", 0),
)
self._logger.info("wiring_summary", **_wiring_summary_fields(wiring))

def _log_server_info(self) -> None:
"""Log server configuration — like Spring Boot's 'Netty started on port 8080'."""
Expand Down
24 changes: 24 additions & 0 deletions tests/context/test_wiring.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,30 @@ async def handle_order(self, msg):
assert ctx.wiring_counts.get("message_listeners", 0) == 0


# --- Test: EDA @event_listener wiring ---


class TestEdaEventListenerWiring:
@pytest.mark.asyncio
async def test_eda_event_listener_count_tracked(self):
"""Context-driven @event_listener beans are subscribed and counted under event_listeners_eda."""
from pyfly.eda.decorators import event_listener

@service
class OrderEvents:
@event_listener(["order.placed"])
async def on_placed(self, envelope) -> None:
pass

ctx = ApplicationContext(Config({"pyfly": {"eda": {"provider": "memory"}}}))
ctx.register_bean(OrderEvents)
await ctx.start()
try:
assert ctx.wiring_counts.get("event_listeners_eda", 0) >= 1
finally:
await ctx.stop()


# --- Test: @command_handler / @query_handler wiring ---


Expand Down
48 changes: 48 additions & 0 deletions tests/core/test_wiring_summary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright 2026 Firefly Software Foundation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The startup ``wiring_summary`` log must surface every decorator-wiring count.

Regression: EDA ``@event_listener`` subscriptions (``event_listeners_eda``) were
omitted from the summary, so it reported wired EDA listeners as absent.
"""

from __future__ import annotations

from pyfly.core.application import _wiring_summary_fields

_ADVERTISED = (
"event_listeners",
"event_listeners_eda",
"message_listeners",
"cqrs_handlers",
"scheduled_tasks",
"async_methods",
"post_processors",
)


def test_summary_surfaces_eda_event_listeners() -> None:
fields = _wiring_summary_fields({"event_listeners_eda": 3})
assert fields["event_listeners_eda"] == 3


def test_summary_maps_scheduled_alias() -> None:
# the raw counter key is "scheduled"; the summary exposes it as "scheduled_tasks"
assert _wiring_summary_fields({"scheduled": 2})["scheduled_tasks"] == 2


def test_summary_includes_all_advertised_counts_defaulting_to_zero() -> None:
fields = _wiring_summary_fields({})
for key in _ADVERTISED:
assert fields[key] == 0
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