diff --git a/.gitignore b/.gitignore
index c7f9b90..5546dd6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,6 @@ docs/plans/
# Playwright MCP verification artifacts
.playwright-mcp/
.audit/
+
+# Superpowers brainstorming visual companion scratch
+.superpowers/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ae08a73..41f0bd6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
diff --git a/README.md b/README.md
index fb2b36c..805b081 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
diff --git a/pyproject.toml b/pyproject.toml
index 1debcd0..d16b0bc 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.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"
diff --git a/src/pyfly/__init__.py b/src/pyfly/__init__.py
index 3267959..5e25978 100644
--- a/src/pyfly/__init__.py
+++ b/src/pyfly/__init__.py
@@ -13,4 +13,4 @@
# limitations under the License.
"""PyFly — Enterprise Python Framework."""
-__version__ = "26.06.39"
+__version__ = "26.06.40"
diff --git a/src/pyfly/context/__init__.py b/src/pyfly/context/__init__.py
index 3469cc4..0347393 100644
--- a/src/pyfly/context/__init__.py
+++ b/src/pyfly/context/__init__.py
@@ -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 (
@@ -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",
]
diff --git a/src/pyfly/context/condition_evaluator.py b/src/pyfly/context/condition_evaluator.py
index d990494..51d5105 100644
--- a/src/pyfly/context/condition_evaluator.py
+++ b/src/pyfly/context/condition_evaluator.py
@@ -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:
@@ -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":
@@ -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)
diff --git a/src/pyfly/context/conditions.py b/src/pyfly/context/conditions.py
index 3565807..5c92f7b 100644
--- a/src/pyfly/context/conditions.py
+++ b/src/pyfly/context/conditions.py
@@ -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.
diff --git a/tests/context/test_conditional_on_family.py b/tests/context/test_conditional_on_family.py
new file mode 100644
index 0000000..821b708
--- /dev/null
+++ b/tests/context/test_conditional_on_family.py
@@ -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
diff --git a/uv.lock b/uv.lock
index bbd75e2..35e354d 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1981,7 +1981,7 @@ wheels = [
[[package]]
name = "pyfly"
-version = "26.6.39"
+version = "26.6.40"
source = { editable = "." }
dependencies = [
{ name = "pydantic" },