From c5ca2a22f7bf50a8494ff9ca059ba7b42e350e39 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Oct 2024 08:19:08 +0000 Subject: [PATCH 01/17] DNM: Ensure config_flow translations are available --- tests/components/conftest.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index e04639d687a87..c456b118aa70b 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -11,8 +11,11 @@ from aiohasupervisor.models import StoreInfo import pytest +from homeassistant.config_entries import ConfigFlowResult from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowHandler, FlowManager, FlowResultType +from homeassistant.helpers.translation import async_get_translations if TYPE_CHECKING: from homeassistant.components.hassio import AddonManager @@ -438,3 +441,31 @@ def supervisor_client() -> Generator[AsyncMock]: ), ): yield supervisor_client + + +@pytest.fixture(autouse=True) +def check_config_translations(hass: HomeAssistant) -> Generator[None]: + """Ensure config_flow translations are available.""" + _original_call = hass.config_entries.flow._async_handle_step + + async def _async_handle_step( + self: FlowManager, flow: FlowHandler, *args + ) -> ConfigFlowResult: + result = await _original_call(flow, *args) + if result["type"] is FlowResultType.ABORT: + if not (reason := result.get("reason")): + raise ValueError("Abort string not found") + translations = await async_get_translations( + self.hass, "en", "config", [flow.handler] + ) + if f"component.{flow.handler}.config.abort.{reason}" not in translations: + raise ValueError( + f"Abort reason `{reason}` not found in `strings.json`: {translations}" + ) + return result + + with patch( + "homeassistant.data_entry_flow.FlowManager._async_handle_step", + _async_handle_step, + ): + yield From fb566b19c5d212fbfebb2f86e9bdc060cd6c1ad9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:37:31 +0200 Subject: [PATCH 02/17] Update conftest.py --- tests/components/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index c456b118aa70b..19526fbe5d638 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -460,7 +460,7 @@ async def _async_handle_step( ) if f"component.{flow.handler}.config.abort.{reason}" not in translations: raise ValueError( - f"Abort reason `{reason}` not found in `strings.json`: {translations}" + f"Abort reason `{reason}` not found in `strings.json`" ) return result From 6a324ac163f0acd7d0ac59010bfa1e6799271a5b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:42:25 +0200 Subject: [PATCH 03/17] Ruff --- tests/components/conftest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 19526fbe5d638..4d8c3d32b0a84 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -459,9 +459,7 @@ async def _async_handle_step( self.hass, "en", "config", [flow.handler] ) if f"component.{flow.handler}.config.abort.{reason}" not in translations: - raise ValueError( - f"Abort reason `{reason}` not found in `strings.json`" - ) + raise ValueError(f"Abort reason `{reason}` not found in `strings.json`") return result with patch( From 1b98a3e3e43d6f6e45ced94d2faeceddb2057f3d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:42:52 +0200 Subject: [PATCH 04/17] Update conftest.py --- tests/components/conftest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 4d8c3d32b0a84..d407fd3f3dca1 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -453,8 +453,6 @@ async def _async_handle_step( ) -> ConfigFlowResult: result = await _original_call(flow, *args) if result["type"] is FlowResultType.ABORT: - if not (reason := result.get("reason")): - raise ValueError("Abort string not found") translations = await async_get_translations( self.hass, "en", "config", [flow.handler] ) From 17e365046033875793470ce01a21c2004f32f60e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:57:47 +0200 Subject: [PATCH 05/17] Update conftest.py --- tests/components/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index d407fd3f3dca1..da80c33da5b3c 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -452,7 +452,7 @@ async def _async_handle_step( self: FlowManager, flow: FlowHandler, *args ) -> ConfigFlowResult: result = await _original_call(flow, *args) - if result["type"] is FlowResultType.ABORT: + if result["type"] is FlowResultType.ABORT and (reason := result.get("reason")): translations = await async_get_translations( self.hass, "en", "config", [flow.handler] ) From 25c2ecc75b5eb095add9788be149b3f1408125d6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Oct 2024 09:42:04 +0000 Subject: [PATCH 06/17] Tweak _original source --- tests/components/conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index da80c33da5b3c..ce0cad079ca08 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -444,14 +444,14 @@ def supervisor_client() -> Generator[AsyncMock]: @pytest.fixture(autouse=True) -def check_config_translations(hass: HomeAssistant) -> Generator[None]: +def check_config_translations() -> Generator[None]: """Ensure config_flow translations are available.""" - _original_call = hass.config_entries.flow._async_handle_step + _original = FlowManager._async_handle_step async def _async_handle_step( self: FlowManager, flow: FlowHandler, *args ) -> ConfigFlowResult: - result = await _original_call(flow, *args) + result = await _original(self, flow, *args) if result["type"] is FlowResultType.ABORT and (reason := result.get("reason")): translations = await async_get_translations( self.hass, "en", "config", [flow.handler] From 2e09acacc71b57bec354e6b64b01a813624b8a2f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Oct 2024 09:59:19 +0000 Subject: [PATCH 07/17] Ignore for options and discovery --- tests/components/conftest.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index ce0cad079ca08..9585330de1191 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -11,7 +11,7 @@ from aiohasupervisor.models import StoreInfo import pytest -from homeassistant.config_entries import ConfigFlowResult +from homeassistant.config_entries import ConfigEntriesFlowManager, FlowResult from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowHandler, FlowManager, FlowResultType @@ -450,14 +450,21 @@ def check_config_translations() -> Generator[None]: async def _async_handle_step( self: FlowManager, flow: FlowHandler, *args - ) -> ConfigFlowResult: + ) -> FlowResult: result = await _original(self, flow, *args) - if result["type"] is FlowResultType.ABORT and (reason := result.get("reason")): + if ( + result["type"] is FlowResultType.ABORT + and isinstance(self, ConfigEntriesFlowManager) + and flow.source not in {"bluetooth", "hardware", "usb", "zeroconf"} + ): + reason = result.get("reason") translations = await async_get_translations( self.hass, "en", "config", [flow.handler] ) if f"component.{flow.handler}.config.abort.{reason}" not in translations: - raise ValueError(f"Abort reason `{reason}` not found in `strings.json`") + raise ValueError( + f"Translation not found for {flow.source} abort reason `{reason}`" + ) return result with patch( From e15615e8887a687366a0c2475b5289f922710023 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:01:58 +0000 Subject: [PATCH 08/17] Ignore dhcp --- tests/components/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 9585330de1191..a1eb70b61c87f 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -455,7 +455,7 @@ async def _async_handle_step( if ( result["type"] is FlowResultType.ABORT and isinstance(self, ConfigEntriesFlowManager) - and flow.source not in {"bluetooth", "hardware", "usb", "zeroconf"} + and flow.source not in {"bluetooth", "dhcp", "hardware", "usb", "zeroconf"} ): reason = result.get("reason") translations = await async_get_translations( From 903e72b997ee802b295e0faafee41f902b0c1943 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:43:22 +0000 Subject: [PATCH 09/17] Ignore based on DISCOVERY_SOURCES --- tests/components/conftest.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index a1eb70b61c87f..ecd908e03988a 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -11,7 +11,11 @@ from aiohasupervisor.models import StoreInfo import pytest -from homeassistant.config_entries import ConfigEntriesFlowManager, FlowResult +from homeassistant.config_entries import ( + DISCOVERY_SOURCES, + ConfigEntriesFlowManager, + FlowResult, +) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowHandler, FlowManager, FlowResultType @@ -455,7 +459,7 @@ async def _async_handle_step( if ( result["type"] is FlowResultType.ABORT and isinstance(self, ConfigEntriesFlowManager) - and flow.source not in {"bluetooth", "dhcp", "hardware", "usb", "zeroconf"} + and flow.source not in DISCOVERY_SOURCES ): reason = result.get("reason") translations = await async_get_translations( From 110da587c027eb1b6c8a6b8f9cf7df89c28f1c96 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:38:09 +0000 Subject: [PATCH 10/17] Trap more errors --- tests/components/conftest.py | 42 +++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index ecd908e03988a..14a19f382df62 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -15,6 +15,7 @@ DISCOVERY_SOURCES, ConfigEntriesFlowManager, FlowResult, + OptionsFlowManager, ) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant @@ -447,6 +448,15 @@ def supervisor_client() -> Generator[AsyncMock]: yield supervisor_client +async def _ensure_translation_exists( + hass: HomeAssistant, category: str, component: str, key: str +) -> None: + """Raise if translation doesn't exist.""" + translations = await async_get_translations(hass, "en", category, [component]) + if f"component.{component}.{category}.{key}" not in translations: + raise ValueError(f"Translation not found for {component}: `{category}.{key}`") + + @pytest.fixture(autouse=True) def check_config_translations() -> Generator[None]: """Ensure config_flow translations are available.""" @@ -456,19 +466,35 @@ async def _async_handle_step( self: FlowManager, flow: FlowHandler, *args ) -> FlowResult: result = await _original(self, flow, *args) + if isinstance(self, ConfigEntriesFlowManager): + category = "config" + component = flow.handler + elif isinstance(self, OptionsFlowManager): + category = "options" + component = flow.hass.config_entries.async_get_entry(flow.handler).domain + else: + return result + + if result["type"] is FlowResultType.FORM: + if errors := result.get("errors"): + for error in errors.values(): + await _ensure_translation_exists( + self.hass, + category, + component, + f"error.{error}", + ) if ( result["type"] is FlowResultType.ABORT - and isinstance(self, ConfigEntriesFlowManager) and flow.source not in DISCOVERY_SOURCES ): - reason = result.get("reason") - translations = await async_get_translations( - self.hass, "en", "config", [flow.handler] + await _ensure_translation_exists( + self.hass, + category, + component, + f"abort.{result["reason"]}", ) - if f"component.{flow.handler}.config.abort.{reason}" not in translations: - raise ValueError( - f"Translation not found for {flow.source} abort reason `{reason}`" - ) + return result with patch( From c929b56c387198ac5b6c4a3b09ecbbd620651ab3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:47:21 +0000 Subject: [PATCH 11/17] Add more checks --- tests/components/conftest.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 14a19f382df62..0e8574f4ae237 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -476,20 +476,29 @@ async def _async_handle_step( return result if result["type"] is FlowResultType.FORM: + if data_schema := result.get("data_schema"): + for key in data_schema.schema: + await _ensure_translation_exists( + flow.hass, + category, + component, + f"step.{result['step_id']}.data.{key}", + ) if errors := result.get("errors"): for error in errors.values(): await _ensure_translation_exists( - self.hass, + flow.hass, category, component, f"error.{error}", ) + if ( result["type"] is FlowResultType.ABORT and flow.source not in DISCOVERY_SOURCES ): await _ensure_translation_exists( - self.hass, + flow.hass, category, component, f"abort.{result["reason"]}", From 973c2df02d8e04d37b62aa2fee5556e7bd295b69 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:20:00 +0000 Subject: [PATCH 12/17] Add ability to ignore --- tests/components/conftest.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 0e8574f4ae237..acd7382af72d1 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -448,13 +448,25 @@ def supervisor_client() -> Generator[AsyncMock]: yield supervisor_client +_IGNORE_TRANSLATION_VIOLATIONS = { + "component.airvisual.config.step.user.data.type", # Uses description +} + + async def _ensure_translation_exists( hass: HomeAssistant, category: str, component: str, key: str ) -> None: """Raise if translation doesn't exist.""" + full_key = f"component.{component}.{category}.{key}" + if full_key in _IGNORE_TRANSLATION_VIOLATIONS: + return + translations = await async_get_translations(hass, "en", category, [component]) - if f"component.{component}.{category}.{key}" not in translations: - raise ValueError(f"Translation not found for {component}: `{category}.{key}`") + if full_key not in translations: + raise ValueError( + f"Translation not found for {component}: `{category}.{key}` " + f"(see homeassistant/components/{component}/config_flow.py)" + ) @pytest.fixture(autouse=True) From 4788cce1ce78c4a2677918d0d7602995c700fcd7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:56:00 +0000 Subject: [PATCH 13/17] Add ignores --- tests/components/conftest.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index acd7382af72d1..fbd51f3702e04 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -450,6 +450,8 @@ def supervisor_client() -> Generator[AsyncMock]: _IGNORE_TRANSLATION_VIOLATIONS = { "component.airvisual.config.step.user.data.type", # Uses description + "component.ambient_network.config.step.user.data.location", # Uses description + "component.axis.options.step.configure_stream.data.video_source", # Uses description } @@ -464,8 +466,9 @@ async def _ensure_translation_exists( translations = await async_get_translations(hass, "en", category, [component]) if full_key not in translations: raise ValueError( - f"Translation not found for {component}: `{category}.{key}` " - f"(see homeassistant/components/{component}/config_flow.py)" + f"Translation not found for {component}: `{category}.{key}`. " + f"Please add to homeassistant/components/{component}/strings.json " + "or add to _IGNORE_TRANSLATION_VIOLATIONS." ) From 293227098065399b24bb012f063e2661058008a7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Oct 2024 13:51:14 +0000 Subject: [PATCH 14/17] Add check for schema sections --- tests/components/conftest.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index fbd51f3702e04..2633335f97e83 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -19,7 +19,12 @@ ) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowHandler, FlowManager, FlowResultType +from homeassistant.data_entry_flow import ( + FlowHandler, + FlowManager, + FlowResultType, + section, +) from homeassistant.helpers.translation import async_get_translations if TYPE_CHECKING: @@ -492,13 +497,22 @@ async def _async_handle_step( if result["type"] is FlowResultType.FORM: if data_schema := result.get("data_schema"): - for key in data_schema.schema: - await _ensure_translation_exists( - flow.hass, - category, - component, - f"step.{result['step_id']}.data.{key}", - ) + for key, value in data_schema.schema.items(): + if isinstance(value, section): + for sub_key in value.schema.schema: + await _ensure_translation_exists( + flow.hass, + category, + component, + f"step.{result['step_id']}.sections.{key}.data.{sub_key}", + ) + else: + await _ensure_translation_exists( + flow.hass, + category, + component, + f"step.{result['step_id']}.data.{key}", + ) if errors := result.get("errors"): for error in errors.values(): await _ensure_translation_exists( From a01ea86d5e980e19008590e77174705671de9743 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Oct 2024 07:35:59 +0000 Subject: [PATCH 15/17] Adjust description for the translation violations --- tests/components/conftest.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 2633335f97e83..d202aa5756ae2 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -454,9 +454,12 @@ def supervisor_client() -> Generator[AsyncMock]: _IGNORE_TRANSLATION_VIOLATIONS = { - "component.airvisual.config.step.user.data.type", # Uses description - "component.ambient_network.config.step.user.data.location", # Uses description - "component.axis.options.step.configure_stream.data.video_source", # Uses description + # These data input have a step description and it would be redundant to add + # a separate description for the data input + "component.airvisual.config.step.user.data.type", + "component.ambient_network.config.step.user.data.location", + "component.axis.options.step.configure_stream.data.video_source", + "config.step.pick_implementation.data.implementation", } From bd5b5d89b29a8ed717ea0f9864c3d2cd31c42ac6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Oct 2024 07:40:19 +0000 Subject: [PATCH 16/17] Fix incorrect violation --- tests/components/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index d202aa5756ae2..3c0af399fd3bc 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -459,7 +459,7 @@ def supervisor_client() -> Generator[AsyncMock]: "component.airvisual.config.step.user.data.type", "component.ambient_network.config.step.user.data.location", "component.axis.options.step.configure_stream.data.video_source", - "config.step.pick_implementation.data.implementation", + "component.tesla_fleet.config.step.pick_implementation.data.implementation", } From dfb19a47a668bbd5351994f51e5781fdfea4c07f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Oct 2024 07:58:30 +0000 Subject: [PATCH 17/17] Be nicer about data translations --- tests/components/conftest.py | 39 +++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 3c0af399fd3bc..f3603944c235f 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -453,31 +453,34 @@ def supervisor_client() -> Generator[AsyncMock]: yield supervisor_client -_IGNORE_TRANSLATION_VIOLATIONS = { - # These data input have a step description and it would be redundant to add - # a separate description for the data input - "component.airvisual.config.step.user.data.type", - "component.ambient_network.config.step.user.data.location", - "component.axis.options.step.configure_stream.data.video_source", - "component.tesla_fleet.config.step.pick_implementation.data.implementation", -} - - async def _ensure_translation_exists( hass: HomeAssistant, category: str, component: str, key: str ) -> None: """Raise if translation doesn't exist.""" - full_key = f"component.{component}.{category}.{key}" - if full_key in _IGNORE_TRANSLATION_VIOLATIONS: + translations = await async_get_translations(hass, "en", category, [component]) + if f"component.{component}.{category}.{key}" in translations: return - translations = await async_get_translations(hass, "en", category, [component]) - if full_key not in translations: - raise ValueError( - f"Translation not found for {component}: `{category}.{key}`. " - f"Please add to homeassistant/components/{component}/strings.json " - "or add to _IGNORE_TRANSLATION_VIOLATIONS." + key_parts = key.split(".") + # Ignore step data translations if title or description exists + if ( + len(key_parts) >= 3 + and key_parts[0] == "step" + and key_parts[2] == "data" + and ( + f"component.{component}.{category}.{key_parts[0]}.{key_parts[1]}.description" + in translations + or f"component.{component}.{category}.{key_parts[0]}.{key_parts[1]}.title" + in translations ) + ): + return + + raise ValueError( + f"Translation not found for {component}: `{category}.{key}`. " + f"Please add to homeassistant/components/{component}/strings.json " + "or add to _IGNORE_TRANSLATION_VIOLATIONS." + ) @pytest.fixture(autouse=True)