Refactor translation checks (#130585)

* Refactor translation checks

* Adjust

* Improve

* Restore await

* Delay pytest.fail until the end of the test
This commit is contained in:
epenet 2024-11-14 16:26:05 +01:00 committed by GitHub
parent 472414a8d6
commit c7ee7dc880
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -26,7 +26,12 @@ from homeassistant.config_entries import (
) )
from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowHandler, FlowManager, FlowResultType from homeassistant.data_entry_flow import (
FlowContext,
FlowHandler,
FlowManager,
FlowResultType,
)
from homeassistant.helpers.translation import async_get_translations from homeassistant.helpers.translation import async_get_translations
if TYPE_CHECKING: if TYPE_CHECKING:
@ -557,12 +562,12 @@ def _validate_translation_placeholders(
description_placeholders is None description_placeholders is None
or placeholder not in description_placeholders or placeholder not in description_placeholders
): ):
pytest.fail( ignore_translations[full_key] = (
f"Description not found for placeholder `{placeholder}` in {full_key}" f"Description not found for placeholder `{placeholder}` in {full_key}"
) )
async def _ensure_translation_exists( async def _validate_translation(
hass: HomeAssistant, hass: HomeAssistant,
ignore_translations: dict[str, StoreInfo], ignore_translations: dict[str, StoreInfo],
category: str, category: str,
@ -588,7 +593,7 @@ async def _ensure_translation_exists(
ignore_translations[full_key] = "used" ignore_translations[full_key] = "used"
return return
pytest.fail( ignore_translations[full_key] = (
f"Translation not found for {component}: `{category}.{key}`. " f"Translation not found for {component}: `{category}.{key}`. "
f"Please add to homeassistant/components/{component}/strings.json" f"Please add to homeassistant/components/{component}/strings.json"
) )
@ -604,84 +609,106 @@ def ignore_translations() -> str | list[str]:
return [] return []
async def _check_config_flow_result_translations(
manager: FlowManager,
flow: FlowHandler,
result: FlowResult[FlowContext, str],
ignore_translations: dict[str, str],
) -> None:
if isinstance(manager, ConfigEntriesFlowManager):
category = "config"
integration = flow.handler
elif isinstance(manager, OptionsFlowManager):
category = "options"
integration = flow.hass.config_entries.async_get_entry(flow.handler).domain
else:
return
# Check if this flow has been seen before
# Gets set to False on first run, and to True on subsequent runs
setattr(flow, "__flow_seen_before", hasattr(flow, "__flow_seen_before"))
if result["type"] is FlowResultType.FORM:
if step_id := result.get("step_id"):
# neither title nor description are required
# - title defaults to integration name
# - description is optional
for header in ("title", "description"):
await _validate_translation(
flow.hass,
ignore_translations,
category,
integration,
f"step.{step_id}.{header}",
result["description_placeholders"],
translation_required=False,
)
if errors := result.get("errors"):
for error in errors.values():
await _validate_translation(
flow.hass,
ignore_translations,
category,
integration,
f"error.{error}",
result["description_placeholders"],
)
return
if result["type"] is FlowResultType.ABORT:
# We don't need translations for a discovery flow which immediately
# aborts, since such flows won't be seen by users
if not flow.__flow_seen_before and flow.source in DISCOVERY_SOURCES:
return
await _validate_translation(
flow.hass,
ignore_translations,
category,
integration,
f"abort.{result["reason"]}",
result["description_placeholders"],
)
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def check_config_translations(ignore_translations: str | list[str]) -> Generator[None]: def check_translations(ignore_translations: str | list[str]) -> Generator[None]:
"""Ensure config_flow translations are available.""" """Check that translation requirements are met.
Current checks:
- data entry flow results (ConfigFlow/OptionsFlow)
"""
if not isinstance(ignore_translations, list): if not isinstance(ignore_translations, list):
ignore_translations = [ignore_translations] ignore_translations = [ignore_translations]
_ignore_translations = {k: "unused" for k in ignore_translations} _ignore_translations = {k: "unused" for k in ignore_translations}
_original = FlowManager._async_handle_step
async def _async_handle_step( # Keep reference to original functions
_original_flow_manager_async_handle_step = FlowManager._async_handle_step
# Prepare override functions
async def _flow_manager_async_handle_step(
self: FlowManager, flow: FlowHandler, *args self: FlowManager, flow: FlowHandler, *args
) -> FlowResult: ) -> FlowResult:
result = await _original(self, flow, *args) result = await _original_flow_manager_async_handle_step(self, flow, *args)
if isinstance(self, ConfigEntriesFlowManager): await _check_config_flow_result_translations(
category = "config" self, flow, result, _ignore_translations
component = flow.handler )
elif isinstance(self, OptionsFlowManager):
category = "options"
component = flow.hass.config_entries.async_get_entry(flow.handler).domain
else:
return result
# Check if this flow has been seen before
# Gets set to False on first run, and to True on subsequent runs
setattr(flow, "__flow_seen_before", hasattr(flow, "__flow_seen_before"))
if result["type"] is FlowResultType.FORM:
if step_id := result.get("step_id"):
# neither title nor description are required
# - title defaults to integration name
# - description is optional
for header in ("title", "description"):
await _ensure_translation_exists(
flow.hass,
_ignore_translations,
category,
component,
f"step.{step_id}.{header}",
result["description_placeholders"],
translation_required=False,
)
if errors := result.get("errors"):
for error in errors.values():
await _ensure_translation_exists(
flow.hass,
_ignore_translations,
category,
component,
f"error.{error}",
result["description_placeholders"],
)
return result
if result["type"] is FlowResultType.ABORT:
# We don't need translations for a discovery flow which immediately
# aborts, since such flows won't be seen by users
if not flow.__flow_seen_before and flow.source in DISCOVERY_SOURCES:
return result
await _ensure_translation_exists(
flow.hass,
_ignore_translations,
category,
component,
f"abort.{result["reason"]}",
result["description_placeholders"],
)
return result return result
# Use override functions
with patch( with patch(
"homeassistant.data_entry_flow.FlowManager._async_handle_step", "homeassistant.data_entry_flow.FlowManager._async_handle_step",
_async_handle_step, _flow_manager_async_handle_step,
): ):
yield yield
# Run final checks
unused_ignore = [k for k, v in _ignore_translations.items() if v == "unused"] unused_ignore = [k for k, v in _ignore_translations.items() if v == "unused"]
if unused_ignore: if unused_ignore:
pytest.fail( pytest.fail(
f"Unused ignore translations: {', '.join(unused_ignore)}. " f"Unused ignore translations: {', '.join(unused_ignore)}. "
"Please remove them from the ignore_translations fixture." "Please remove them from the ignore_translations fixture."
) )
for description in _ignore_translations.values():
if description not in {"used", "unused"}:
pytest.fail(description)