From dd80157dc773a3fb58b31a72f9dfe75ff8a3bd6b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 24 Feb 2024 11:31:25 -1000 Subject: [PATCH] Load translations at setup time if they were not loaded at bootstrap (#110921) --- homeassistant/helpers/translation.py | 40 +++++++--------------------- homeassistant/setup.py | 19 +++++++++++-- tests/helpers/test_translation.py | 16 +---------- tests/test_setup.py | 14 +++++++++- 4 files changed, 40 insertions(+), 49 deletions(-) diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index c07eb6874b0..c2caecaa184 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -5,10 +5,9 @@ import asyncio from collections.abc import Iterable, Mapping import logging import string -from typing import TYPE_CHECKING, Any +from typing import Any from homeassistant.const import ( - EVENT_COMPONENT_LOADED, EVENT_CORE_CONFIG_UPDATE, STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -498,35 +497,6 @@ def async_setup(hass: HomeAssistant) -> None: _LOGGER.debug("Loading translations for language: %s", new_language) await cache.async_load(new_language, hass.config.components) - @callback - def _async_load_translations_for_component_filter(event: Event) -> bool: - """Filter out unwanted events.""" - component: str | None = event.data.get("component") - # Platforms don't have their own translations, skip them - return bool( - component - and "." not in component - and not cache.async_is_loaded(hass.config.language, {component}) - ) - - async def _async_load_translations_for_component(event: Event) -> None: - """Load translations for a component.""" - component: str | None = event.data.get("component") - if TYPE_CHECKING: - assert component is not None - language = hass.config.language - _LOGGER.debug( - "Loading translations for language: %s and component: %s", - language, - component, - ) - await cache.async_load(language, {component}) - - hass.bus.async_listen( - EVENT_COMPONENT_LOADED, - _async_load_translations_for_component, - event_filter=_async_load_translations_for_component_filter, - ) hass.bus.async_listen( EVENT_CORE_CONFIG_UPDATE, _async_load_translations, @@ -541,6 +511,14 @@ async def async_load_integrations(hass: HomeAssistant, integrations: set[str]) - ) +@callback +def async_translations_loaded(hass: HomeAssistant, components: set[str]) -> bool: + """Return if the given components are loaded for the language.""" + return _async_get_translations_cache(hass).async_is_loaded( + hass.config.language, components + ) + + @callback def async_translate_state( hass: HomeAssistant, diff --git a/homeassistant/setup.py b/homeassistant/setup.py index feb8d857f10..ed54a76dd3d 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -25,6 +25,7 @@ from .core import ( callback, ) from .exceptions import DependencyError, HomeAssistantError +from .helpers import translation from .helpers.issue_registry import IssueSeverity, async_create_issue from .helpers.typing import ConfigType, EventType from .util import ensure_unique_string @@ -244,7 +245,7 @@ async def _async_process_dependencies( return failed -async def _async_setup_component( +async def _async_setup_component( # noqa: C901 hass: core.HomeAssistant, domain: str, config: ConfigType ) -> bool: """Set up a component for Home Assistant. @@ -343,7 +344,19 @@ async def _async_setup_component( start = timer() _LOGGER.info("Setting up %s", domain) - with async_start_setup(hass, [domain]): + integration_set = {domain} + + load_translations_task: asyncio.Task[None] | None = None + if not translation.async_translations_loaded(hass, integration_set): + # For most cases we expect the translations are already + # loaded since we try to load them in bootstrap ahead of time. + # If for some reason the background task in bootstrap was too slow + # or the integration was added after bootstrap, we will load them here. + load_translations_task = asyncio.create_task( + translation.async_load_integrations(hass, integration_set) + ) + + with async_start_setup(hass, integration_set): if hasattr(component, "PLATFORM_SCHEMA"): # Entity components have their own warning warn_task = None @@ -409,6 +422,8 @@ async def _async_setup_component( await asyncio.sleep(0) await hass.config_entries.flow.async_wait_import_flow_initialized(domain) + if load_translations_task: + await load_translations_task # Add to components before the entry.async_setup # call to avoid a deadlock when forwarding platforms hass.config.components.add(domain) diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 18206d1aa38..210378c5812 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -8,7 +8,7 @@ from unittest.mock import Mock, call, patch import pytest from homeassistant import loader -from homeassistant.const import EVENT_COMPONENT_LOADED, EVENT_CORE_CONFIG_UPDATE +from homeassistant.const import EVENT_CORE_CONFIG_UPDATE from homeassistant.core import HomeAssistant from homeassistant.helpers import translation from homeassistant.loader import async_get_integration @@ -605,20 +605,6 @@ async def test_setup(hass: HomeAssistant): """Test the setup load listeners helper.""" translation.async_setup(hass) - with patch( - "homeassistant.helpers.translation._TranslationCache.async_load", - ) as mock: - hass.bus.async_fire(EVENT_COMPONENT_LOADED, {"component": "loaded_component"}) - await hass.async_block_till_done() - mock.assert_called_once_with(hass.config.language, {"loaded_component"}) - - with patch( - "homeassistant.helpers.translation._TranslationCache.async_load", - ) as mock: - hass.bus.async_fire(EVENT_COMPONENT_LOADED, {"component": "config.component"}) - await hass.async_block_till_done() - mock.assert_not_called() - # Should not be called if the language is the current language with patch( "homeassistant.helpers.translation._TranslationCache.async_load", diff --git a/tests/test_setup.py b/tests/test_setup.py index 9332157e066..cb0d4e7084b 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -11,7 +11,7 @@ from homeassistant import config_entries, setup from homeassistant.const import EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import discovery +from homeassistant.helpers import discovery, translation from homeassistant.helpers.config_validation import ( PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -801,3 +801,15 @@ async def test_setup_config_entry_from_yaml( caplog.clear() hass.data.pop(setup.DATA_SETUP) hass.config.components.remove("test_integration_only_entry") + + +async def test_loading_component_loads_translations(hass: HomeAssistant) -> None: + """Test that loading a component loads translations.""" + assert translation.async_translations_loaded(hass, {"comp"}) is False + mock_setup = Mock(return_value=True) + + mock_integration(hass, MockModule("comp", setup=mock_setup)) + + assert await setup.async_setup_component(hass, "comp", {}) + assert mock_setup.called + assert translation.async_translations_loaded(hass, {"comp"}) is True