From 3aecec5082f063ebc1beb81247377b34dfc48deb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 23 Feb 2024 10:48:47 -1000 Subject: [PATCH] Avoid rechecking for missing platforms in the loader (#111204) --- homeassistant/loader.py | 22 +++++++++++++++++----- tests/test_loader.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 013873243df..ee31154598a 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -53,6 +53,7 @@ _LOGGER = logging.getLogger(__name__) DATA_COMPONENTS = "components" DATA_INTEGRATIONS = "integrations" +DATA_MISSING_PLATFORMS = "missing_platforms" DATA_CUSTOM_COMPONENTS = "custom_components" PACKAGE_CUSTOM_COMPONENTS = "custom_components" PACKAGE_BUILTIN = "homeassistant.components" @@ -185,6 +186,7 @@ def async_setup(hass: HomeAssistant) -> None: _async_mount_config_dir(hass) hass.data[DATA_COMPONENTS] = {} hass.data[DATA_INTEGRATIONS] = {} + hass.data[DATA_MISSING_PLATFORMS] = {} def manifest_from_legacy_module(domain: str, module: ModuleType) -> Manifest: @@ -842,9 +844,19 @@ class Integration: if full_name in cache: return cache[full_name] + missing_platforms_cache: dict[str, ImportError] = self.hass.data[ + DATA_MISSING_PLATFORMS + ] + if full_name in missing_platforms_cache: + raise missing_platforms_cache[full_name] + try: cache[full_name] = self._import_platform(platform_name) - except ImportError: + except ImportError as ex: + if self.domain in cache: + # If the domain is loaded, cache that the platform + # does not exist so we do not try to load it again + missing_platforms_cache[full_name] = ex raise except Exception as err: _LOGGER.exception( @@ -1022,10 +1034,10 @@ def _load_file( Only returns it if also found to be valid. Async friendly. """ - with suppress(KeyError): - return hass.data[DATA_COMPONENTS][comp_or_platform] # type: ignore[no-any-return] - - cache = hass.data[DATA_COMPONENTS] + cache: dict[str, ComponentProtocol] = hass.data[DATA_COMPONENTS] + module: ComponentProtocol | None + if module := cache.get(comp_or_platform): + return module for path in (f"{base}.{comp_or_platform}" for base in base_paths): try: diff --git a/tests/test_loader.py b/tests/test_loader.py index 501764bd022..3745b85b54c 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -188,6 +188,41 @@ async def test_get_integration_exceptions(hass: HomeAssistant) -> None: assert hue_light == integration.get_platform("light") +async def test_get_platform_caches_failures_when_component_loaded( + hass: HomeAssistant, +) -> None: + """Test get_platform cache failures only when the component is loaded.""" + integration = await loader.async_get_integration(hass, "hue") + + with pytest.raises(ImportError), patch( + "homeassistant.loader.importlib.import_module", side_effect=ImportError("Boom") + ): + assert integration.get_component() == hue + + with pytest.raises(ImportError), patch( + "homeassistant.loader.importlib.import_module", side_effect=ImportError("Boom") + ): + assert integration.get_platform("light") == hue_light + + # Hue is not loaded so we should still hit the import_module path + with pytest.raises(ImportError), patch( + "homeassistant.loader.importlib.import_module", side_effect=ImportError("Boom") + ): + assert integration.get_platform("light") == hue_light + + assert integration.get_component() == hue + + # Hue is loaded so we should cache the import_module failure now + with pytest.raises(ImportError), patch( + "homeassistant.loader.importlib.import_module", side_effect=ImportError("Boom") + ): + assert integration.get_platform("light") == hue_light + + # Hue is loaded and the last call should have cached the import_module failure + with pytest.raises(ImportError): + assert integration.get_platform("light") == hue_light + + async def test_get_integration_legacy( hass: HomeAssistant, enable_custom_integrations: None ) -> None: