Detect early base platforms in bootstrap (#140359)

* Detect early base platforms in bootstrap

* Address feedback

* Address feedback
This commit is contained in:
Artur Pragacz 2025-03-20 12:56:45 +01:00 committed by GitHub
parent df0125abdd
commit c9b27cf26e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1546,41 +1546,68 @@ def test_should_rollover_is_always_false() -> None:
async def test_no_base_platforms_loaded_before_recorder(hass: HomeAssistant) -> None: async def test_no_base_platforms_loaded_before_recorder(hass: HomeAssistant) -> None:
"""Verify stage 0 not load base platforms before recorder. """Verify stage 0 not load base platforms before recorder.
If a stage 0 integration has a base platform in its dependencies and If a stage 0 integration implements base platforms or has a base
it loads before the recorder, it may load integrations that expect platform in its dependencies and it loads before the recorder,
the recorder to be loaded. We need to ensure that no stage 0 integration because of platform-based YAML schema, it may inadvertently
has a base platform in its dependencies that loads before the recorder. load integrations that expect the recorder to already be loaded.
We need to ensure that doesn't happen.
""" """
IGNORE_BASE_PLATFORM_FILES = {
# config/scene.py is not a platform
"config": {"scene.py"},
# websocket_api/sensor.py is using the platform YAML schema
# we must not migrate it to an integration key until
# we remove the platform YAML schema support for sensors
"websocket_api": {"sensor.py"},
}
integrations_before_recorder: set[str] = set() integrations_before_recorder: set[str] = set()
for _, integrations, _ in bootstrap.STAGE_0_INTEGRATIONS: for _, integrations, _ in bootstrap.STAGE_0_INTEGRATIONS:
integrations_before_recorder |= integrations integrations_before_recorder |= integrations
if "recorder" in integrations: if "recorder" in integrations:
break break
else:
pytest.fail("recorder not in stage 0")
integrations_or_execs = await loader.async_get_integrations( integrations_or_excs = await loader.async_get_integrations(
hass, integrations_before_recorder hass, integrations_before_recorder
) )
integrations: list[Integration] = [] integrations: dict[str, Integration] = {}
resolve_deps_tasks: list[asyncio.Task[bool]] = [] for domain, integration in integrations_or_excs.items():
for integration in integrations_or_execs.values(): assert not isinstance(integrations_or_excs, Exception)
assert not isinstance(integrations_or_execs, Exception) integrations[domain] = integration
integrations.append(integration)
resolve_deps_tasks.append(integration.resolve_dependencies()) integrations_all_dependencies = await loader.resolve_integrations_dependencies(
hass, integrations.values()
)
all_integrations = integrations.copy()
all_integrations.update(
(domain, loader.async_get_loaded_integration(hass, domain))
for domains in integrations_all_dependencies.values()
for domain in domains
)
problems: dict[str, set[str]] = {}
for domain in integrations:
domain_with_base_platforms_deps = (
integrations_all_dependencies[domain] & BASE_PLATFORMS
)
if domain_with_base_platforms_deps:
problems[domain] = domain_with_base_platforms_deps
assert not problems, (
f"Integrations that are setup before recorder have base platforms in their dependencies: {problems}"
)
await asyncio.gather(*resolve_deps_tasks)
base_platform_py_files = {f"{base_platform}.py" for base_platform in BASE_PLATFORMS} base_platform_py_files = {f"{base_platform}.py" for base_platform in BASE_PLATFORMS}
for integration in integrations:
domain_with_base_platforms_deps = BASE_PLATFORMS.intersection( for domain, integration in all_integrations.items():
integration.all_dependencies integration_base_platforms_files = (
integration._top_level_files & base_platform_py_files
) )
assert not domain_with_base_platforms_deps, ( if ignore := IGNORE_BASE_PLATFORM_FILES.get(domain):
f"{integration.domain} has base platforms in dependencies: " integration_base_platforms_files -= ignore
f"{domain_with_base_platforms_deps}" if integration_base_platforms_files:
) problems[domain] = integration_base_platforms_files
integration_top_level_files = base_platform_py_files.intersection( assert not problems, (
integration._top_level_files f"Integrations that are setup before recorder implement base platforms: {problems}"
)
assert not integration_top_level_files, (
f"{integration.domain} has base platform files in top level files: "
f"{integration_top_level_files}"
) )