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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ docs/plans/
# Playwright MCP verification artifacts
.playwright-mcp/
.audit/

# Superpowers brainstorming visual companion scratch
.superpowers/
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

---

## v26.06.40 (2026-06-07)

### Added (context — more @ConditionalOn* conditions)

- **`@conditional_on_web_application()`** — registers the bean only when a web stack
(Starlette or FastAPI) is importable (Spring `@ConditionalOnWebApplication`).
- **`@conditional_on_resource(path)`** — registers only when the filesystem resource at
*path* exists (Spring `@ConditionalOnResource`).

Both exported from `pyfly.context`. (`@ConditionalOnSingleCandidate` remains on the
roadmap — it needs careful bean-counting around interface-alias registrations.)

## v26.06.39 (2026-06-07)

### Added (config — Spring Boot 2.4+ profile expressions)
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.39-brightgreen" alt="Version: 26.06.39"></a>
<a href="#"><img src="https://img.shields.io/badge/version-26.06.40-brightgreen" alt="Version: 26.06.40"></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.39"
version = "26.6.40"
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.39"
__version__ = "26.06.40"
4 changes: 4 additions & 0 deletions src/pyfly/context/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
conditional_on_expression,
conditional_on_missing_bean,
conditional_on_property,
conditional_on_resource,
conditional_on_web_application,
)
from pyfly.context.environment import Environment
from pyfly.context.events import (
Expand Down Expand Up @@ -52,6 +54,8 @@
"conditional_on_missing_bean",
"conditional_on_expression",
"conditional_on_property",
"conditional_on_resource",
"conditional_on_web_application",
"post_construct",
"pre_destroy",
]
14 changes: 14 additions & 0 deletions src/pyfly/context/condition_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@

from __future__ import annotations

import importlib.util
import logging
import os
from typing import TYPE_CHECKING, Any, cast

if TYPE_CHECKING:
Expand Down Expand Up @@ -90,6 +92,10 @@ def _evaluate(self, cond: dict[str, Any], *, declaring_cls: type | None = None)
result = cond["check"]()
elif cond_type == "on_expression":
result = self._eval_on_expression(cond)
elif cond_type == "on_web_application":
result = self._eval_on_web_application()
elif cond_type == "on_resource":
result = self._eval_on_resource(cond)
elif cond_type == "on_missing_bean":
result = self._eval_on_missing_bean(cond, declaring_cls)
elif cond_type == "on_bean":
Expand Down Expand Up @@ -127,6 +133,14 @@ def _eval_on_expression(self, cond: dict[str, Any]) -> bool:

return bool(evaluate(cond["expression"], self._config))

def _eval_on_web_application(self) -> bool:
"""Match when a web stack (Starlette or FastAPI) is importable."""
return any(importlib.util.find_spec(name) is not None for name in ("starlette", "fastapi"))

def _eval_on_resource(self, cond: dict[str, Any]) -> bool:
"""Match when the filesystem resource at the configured path exists."""
return os.path.exists(cond["path"])

def _eval_on_missing_bean(self, cond: dict[str, Any], declaring_cls: type | None = None) -> bool:
return not self._has_bean_of_type(cond["bean_type"], exclude=declaring_cls)

Expand Down
30 changes: 30 additions & 0 deletions src/pyfly/context/conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,36 @@ def decorator(cls: F) -> F:
return decorator


def conditional_on_web_application() -> Callable[[F], F]:
"""Only register this bean when a web stack is present (Starlette or FastAPI).

Mirrors Spring Boot's ``@ConditionalOnWebApplication``.
"""

def decorator(cls: F) -> F:
conditions = list(cls.__dict__.get("__pyfly_conditions__", []))
conditions.append({"type": "on_web_application"})
cls.__pyfly_conditions__ = conditions # type: ignore[attr-defined]
return cls

return decorator


def conditional_on_resource(path: str) -> Callable[[F], F]:
"""Only register this bean when the filesystem resource at *path* exists.

Mirrors Spring Boot's ``@ConditionalOnResource``.
"""

def decorator(cls: F) -> F:
conditions = list(cls.__dict__.get("__pyfly_conditions__", []))
conditions.append({"type": "on_resource", "path": path})
cls.__pyfly_conditions__ = conditions # type: ignore[attr-defined]
return cls

return decorator


def auto_configuration(cls: T) -> T:
"""Mark a @configuration class as auto-configuration.

Expand Down
53 changes: 53 additions & 0 deletions tests/context/test_conditional_on_family.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 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.
"""@ConditionalOnWebApplication / @ConditionalOnResource (v26.06.40)."""

from __future__ import annotations

from pathlib import Path

from pyfly.container.container import Container
from pyfly.context.condition_evaluator import ConditionEvaluator
from pyfly.context.conditions import conditional_on_resource, conditional_on_web_application
from pyfly.core.config import Config


def _evaluator() -> ConditionEvaluator:
return ConditionEvaluator(Config({}), Container())


def test_conditional_on_web_application() -> None:
@conditional_on_web_application()
class WebBean:
pass

# starlette is installed in the test environment -> a web application
assert _evaluator().should_include(WebBean) is True


def test_conditional_on_resource(tmp_path: Path) -> None:
present_file = tmp_path / "present.txt"
present_file.write_text("hi")

@conditional_on_resource(str(present_file))
class WithResource:
pass

@conditional_on_resource(str(tmp_path / "missing.txt"))
class WithoutResource:
pass

evaluator = _evaluator()
assert evaluator.should_include(WithResource) is True
assert evaluator.should_include(WithoutResource) is False
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