From 8091f511b8fdbda105fafdf2548e14af2ebcec5c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:52:46 +0200 Subject: [PATCH] Reject manifest dependencies on core integrations in hassfest (#169425) --- .../components/recovery_mode/manifest.json | 1 - script/hassfest/dependencies.py | 14 ++++- tests/hassfest/test_dependencies.py | 54 ++++++++++++++++++- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/recovery_mode/manifest.json b/homeassistant/components/recovery_mode/manifest.json index 5837a648ecb..4323b54ac55 100644 --- a/homeassistant/components/recovery_mode/manifest.json +++ b/homeassistant/components/recovery_mode/manifest.json @@ -3,7 +3,6 @@ "name": "Recovery Mode", "codeowners": ["@home-assistant/core"], "config_flow": false, - "dependencies": ["persistent_notification"], "documentation": "https://www.home-assistant.io/integrations/recovery_mode", "integration_type": "system", "quality_scale": "internal" diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index fe48e8a8607..0993b90d348 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -13,6 +13,10 @@ from homeassistant.requirements import DISCOVERY_INTEGRATIONS from . import ast_parse_module from .model import Config, Integration +# Duplicated from homeassistant.bootstrap to avoid importing bootstrap (and its +# eager component pre-imports) into hassfest. Kept in sync via test_dependencies. +CORE_INTEGRATIONS = {"homeassistant", "persistent_notification"} + class ImportCollector(ast.NodeVisitor): """Collect all integrations referenced.""" @@ -86,6 +90,7 @@ class ImportCollector(ast.NodeVisitor): ALLOWED_USED_COMPONENTS = { + *CORE_INTEGRATIONS, *{platform.value for platform in Platform}, # Internal integrations "alert", @@ -95,7 +100,6 @@ ALLOWED_USED_COMPONENTS = { "device_automation", "frontend", "group", - "homeassistant", "input_boolean", "input_button", "input_datetime", @@ -106,7 +110,6 @@ ALLOWED_USED_COMPONENTS = { "media_source", "onboarding", "panel_custom", - "persistent_notification", "person", "script", "shopping_list", @@ -332,6 +335,13 @@ def _validate_dependencies( "dependencies", f"Dependency {dep} does not exist" ) + if dep in CORE_INTEGRATIONS: + integration.add_error( + "dependencies", + f"Dependency {dep} is a core integration and is " + "unconditionally loaded", + ) + def validate( integrations: dict[str, Integration], diff --git a/tests/hassfest/test_dependencies.py b/tests/hassfest/test_dependencies.py index 26ed8a01ba8..46a1443a301 100644 --- a/tests/hassfest/test_dependencies.py +++ b/tests/hassfest/test_dependencies.py @@ -4,7 +4,14 @@ import ast import pytest -from script.hassfest.dependencies import ImportCollector +from script.hassfest.dependencies import ( + CORE_INTEGRATIONS, + ImportCollector, + _validate_dependencies, +) +from script.hassfest.model import Config + +from . import get_integration @pytest.fixture @@ -90,3 +97,48 @@ import homeassistant.components.renamed_absolute as hue "child_import_field", "renamed_absolute", } + + +def test_dependency_on_core_integration_rejected(config: Config) -> None: + """Test that depending on a core integration is rejected.""" + consumer = get_integration("consumer", config) + consumer.manifest["dependencies"] = ["persistent_notification"] + + integrations = { + "consumer": consumer, + "persistent_notification": get_integration("persistent_notification", config), + } + + _validate_dependencies(integrations) + + assert len(consumer.errors) == 1 + assert ( + "Dependency persistent_notification is a core integration" + in consumer.errors[0].error + ) + + +def test_dependency_on_non_core_integration_allowed(config: Config) -> None: + """Test that depending on a non-core integration is not rejected.""" + consumer = get_integration("consumer", config) + consumer.manifest["dependencies"] = ["other"] + + integrations = { + "consumer": consumer, + "other": get_integration("other", config), + } + + _validate_dependencies(integrations) + + assert consumer.errors == [] + + +def test_core_integrations_in_sync_with_bootstrap() -> None: + """Test the duplicated CORE_INTEGRATIONS stays aligned with bootstrap.""" + # Imported here so the rest of the hassfest tests are not slowed down + # by bootstrap's eager component pre-imports. + from homeassistant.bootstrap import ( # noqa: PLC0415 + CORE_INTEGRATIONS as bootstrap_core_integrations, + ) + + assert bootstrap_core_integrations == CORE_INTEGRATIONS