mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Avoid trying to import platforms that do not exist (#112028)
* Avoid trying to import platforms that do not exist * adjust * fixes * cleanup * cleanup * cleanup * Apply suggestions from code review * docs * fixes * fixes * comment * coverage * coverage * coverage * Switch config to use async_get_component This was another path where integrations that were marked to load in the executor would be loaded in the loop * Switch config to use async_get_component/async_get_platform This was another path where integrations that were marked to load in the executor would be loaded in the loop * merge * refactor * refactor * coverage * preen * preen
This commit is contained in:
parent
a253991c6d
commit
c8cb0ff61d
@ -1432,22 +1432,24 @@ async def async_process_component_config( # noqa: C901
|
||||
|
||||
# Check if the integration has a custom config validator
|
||||
config_validator = None
|
||||
try:
|
||||
config_validator = await integration.async_get_platform("config")
|
||||
except ImportError as err:
|
||||
# Filter out import error of the config platform.
|
||||
# If the config platform contains bad imports, make sure
|
||||
# that still fails.
|
||||
if err.name != f"{integration.pkg_path}.config":
|
||||
exc_info = ConfigExceptionInfo(
|
||||
err,
|
||||
ConfigErrorTranslationKey.CONFIG_PLATFORM_IMPORT_ERR,
|
||||
domain,
|
||||
config,
|
||||
integration_docs,
|
||||
)
|
||||
config_exceptions.append(exc_info)
|
||||
return IntegrationConfigInfo(None, config_exceptions)
|
||||
if integration.platform_exists("config") is not False:
|
||||
# If the config platform cannot possibly exist, don't try to load it.
|
||||
try:
|
||||
config_validator = await integration.async_get_platform("config")
|
||||
except ImportError as err:
|
||||
# Filter out import error of the config platform.
|
||||
# If the config platform contains bad imports, make sure
|
||||
# that still fails.
|
||||
if err.name != f"{integration.pkg_path}.config":
|
||||
exc_info = ConfigExceptionInfo(
|
||||
err,
|
||||
ConfigErrorTranslationKey.CONFIG_PLATFORM_IMPORT_ERR,
|
||||
domain,
|
||||
config,
|
||||
integration_docs,
|
||||
)
|
||||
config_exceptions.append(exc_info)
|
||||
return IntegrationConfigInfo(None, config_exceptions)
|
||||
|
||||
if config_validator is not None and hasattr(
|
||||
config_validator, "async_validate_config"
|
||||
|
@ -48,6 +48,10 @@ def _get_platform(
|
||||
)
|
||||
return None
|
||||
|
||||
if integration.platform_exists(platform_name) is False:
|
||||
# If the platform cannot possibly exist, don't bother trying to load it
|
||||
return None
|
||||
|
||||
try:
|
||||
return integration.get_platform(platform_name)
|
||||
except ImportError as err:
|
||||
|
@ -12,6 +12,7 @@ from dataclasses import dataclass
|
||||
import functools as ft
|
||||
import importlib
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
import time
|
||||
@ -976,6 +977,43 @@ class Integration:
|
||||
return platform
|
||||
return self._load_platform(platform_name)
|
||||
|
||||
def platform_exists(self, platform_name: str) -> bool | None:
|
||||
"""Check if a platform exists for an integration.
|
||||
|
||||
Returns True if the platform exists, False if it does not.
|
||||
|
||||
If it cannot be determined if the platform exists without attempting
|
||||
to import the component, it returns None. This will only happen
|
||||
if this function is called before get_component or async_get_component
|
||||
has been called for the integration or the integration failed to load.
|
||||
"""
|
||||
full_name = f"{self.domain}.{platform_name}"
|
||||
|
||||
cache: dict[str, ModuleType] = self.hass.data[DATA_COMPONENTS]
|
||||
if full_name in cache:
|
||||
return True
|
||||
|
||||
missing_platforms_cache: dict[str, ImportError]
|
||||
missing_platforms_cache = self.hass.data[DATA_MISSING_PLATFORMS]
|
||||
if full_name in missing_platforms_cache:
|
||||
return False
|
||||
|
||||
if not (component := cache.get(self.domain)) or not (
|
||||
file := getattr(component, "__file__", None)
|
||||
):
|
||||
return None
|
||||
|
||||
path: pathlib.Path = pathlib.Path(file).parent.joinpath(platform_name)
|
||||
if os.path.exists(path.with_suffix(".py")) or os.path.exists(path):
|
||||
return True
|
||||
|
||||
exc = ModuleNotFoundError(
|
||||
f"Platform {full_name} not found",
|
||||
name=f"{self.pkg_path}.{platform_name}",
|
||||
)
|
||||
missing_platforms_cache[full_name] = exc
|
||||
return False
|
||||
|
||||
def _load_platform(self, platform_name: str) -> ModuleType:
|
||||
"""Load a platform for an integration.
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Test to verify that we can load components."""
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
@ -1193,3 +1194,45 @@ async def test_async_get_platform_raises_after_import_failure(
|
||||
in caplog.text
|
||||
)
|
||||
assert "loaded_executor=False" not in caplog.text
|
||||
|
||||
|
||||
async def test_platform_exists(
|
||||
hass: HomeAssistant, enable_custom_integrations: None
|
||||
) -> None:
|
||||
"""Test platform_exists."""
|
||||
integration = await loader.async_get_integration(hass, "test_integration_platform")
|
||||
assert integration.domain == "test_integration_platform"
|
||||
|
||||
# get_component never called, will return None
|
||||
assert integration.platform_exists("non_existing") is None
|
||||
|
||||
component = integration.get_component()
|
||||
assert component.DOMAIN == "test_integration_platform"
|
||||
|
||||
# component is loaded, should now return False
|
||||
with patch(
|
||||
"homeassistant.loader.os.path.exists", wraps=os.path.exists
|
||||
) as mock_exists:
|
||||
assert integration.platform_exists("non_existing") is False
|
||||
|
||||
# We should check if the file exists
|
||||
assert mock_exists.call_count == 2
|
||||
|
||||
# component is loaded, should now return False
|
||||
with patch(
|
||||
"homeassistant.loader.os.path.exists", wraps=os.path.exists
|
||||
) as mock_exists:
|
||||
assert integration.platform_exists("non_existing") is False
|
||||
|
||||
# We should remember the file does not exist
|
||||
assert mock_exists.call_count == 0
|
||||
|
||||
assert integration.platform_exists("group") is True
|
||||
|
||||
platform = await integration.async_get_platform("group")
|
||||
assert platform.MAGIC == 1
|
||||
|
||||
platform = integration.get_platform("group")
|
||||
assert platform.MAGIC == 1
|
||||
|
||||
assert integration.platform_exists("group") is True
|
||||
|
@ -0,0 +1,9 @@
|
||||
"""Provide a mock package component."""
|
||||
from .const import TEST # noqa: F401
|
||||
|
||||
DOMAIN = "test_integration_platform"
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Mock a successful setup."""
|
||||
return True
|
@ -0,0 +1,7 @@
|
||||
"""Config flow."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
async def _async_has_devices(hass: HomeAssistant) -> bool:
|
||||
return True
|
@ -0,0 +1,2 @@
|
||||
"""Constants for test_package custom component."""
|
||||
TEST = 5
|
@ -0,0 +1,3 @@
|
||||
"""Group."""
|
||||
|
||||
MAGIC = 1
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "test_integration_platform",
|
||||
"name": "Test Integration Platform",
|
||||
"documentation": "http://test-package.io",
|
||||
"requirements": [],
|
||||
"dependencies": [],
|
||||
"codeowners": [],
|
||||
"config_flow": true,
|
||||
"import_executor": true,
|
||||
"version": "1.2.3"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user