Generate HomeAssistantError message from English translations (#113305)
* Fetch exception message from translation cache * Improve tests * Return translation key without path, cleanup * Fetch translations when string variant is requested * Move import * revert changes ConfigValidationError * mypy * Remove _str__ method instead * Type _message for mqtt template exception classes * Revert changes made to test_config.py * Undo changes TemplateError * Follow up comments and test coverage
This commit is contained in:
parent
2bc4a5067d
commit
554aefed42
6 changed files with 136 additions and 13 deletions
|
@ -647,8 +647,6 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||||
elif mode_type == "fan":
|
elif mode_type == "fan":
|
||||||
translation_key = "not_valid_fan_mode"
|
translation_key = "not_valid_fan_mode"
|
||||||
raise ServiceValidationError(
|
raise ServiceValidationError(
|
||||||
f"The {mode_type}_mode {mode} is not a valid {mode_type}_mode:"
|
|
||||||
f" {modes_str}",
|
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key=translation_key,
|
translation_key=translation_key,
|
||||||
translation_placeholders={
|
translation_placeholders={
|
||||||
|
|
|
@ -116,6 +116,8 @@ class MqttOriginInfo(TypedDict, total=False):
|
||||||
class MqttCommandTemplateException(ServiceValidationError):
|
class MqttCommandTemplateException(ServiceValidationError):
|
||||||
"""Handle MqttCommandTemplate exceptions."""
|
"""Handle MqttCommandTemplate exceptions."""
|
||||||
|
|
||||||
|
_message: str
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*args: object,
|
*args: object,
|
||||||
|
@ -227,6 +229,8 @@ class MqttCommandTemplate:
|
||||||
class MqttValueTemplateException(TemplateError):
|
class MqttValueTemplateException(TemplateError):
|
||||||
"""Handle MqttValueTemplate exceptions."""
|
"""Handle MqttValueTemplate exceptions."""
|
||||||
|
|
||||||
|
_message: str
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*args: object,
|
*args: object,
|
||||||
|
|
|
@ -13,6 +13,9 @@ if TYPE_CHECKING:
|
||||||
class HomeAssistantError(Exception):
|
class HomeAssistantError(Exception):
|
||||||
"""General Home Assistant exception occurred."""
|
"""General Home Assistant exception occurred."""
|
||||||
|
|
||||||
|
_message: str | None = None
|
||||||
|
generate_message: bool = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*args: object,
|
*args: object,
|
||||||
|
@ -21,11 +24,42 @@ class HomeAssistantError(Exception):
|
||||||
translation_placeholders: dict[str, str] | None = None,
|
translation_placeholders: dict[str, str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize exception."""
|
"""Initialize exception."""
|
||||||
|
if not args and translation_key and translation_domain:
|
||||||
|
self.generate_message = True
|
||||||
|
args = (translation_key,)
|
||||||
|
|
||||||
super().__init__(*args)
|
super().__init__(*args)
|
||||||
self.translation_domain = translation_domain
|
self.translation_domain = translation_domain
|
||||||
self.translation_key = translation_key
|
self.translation_key = translation_key
|
||||||
self.translation_placeholders = translation_placeholders
|
self.translation_placeholders = translation_placeholders
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return exception message.
|
||||||
|
|
||||||
|
If no message was passed to `__init__`, the exception message is generated from
|
||||||
|
the translation_key. The message will be in English, regardless of the configured
|
||||||
|
language.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self._message:
|
||||||
|
return self._message
|
||||||
|
|
||||||
|
if not self.generate_message:
|
||||||
|
self._message = super().__str__()
|
||||||
|
return self._message
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert self.translation_key is not None
|
||||||
|
assert self.translation_domain is not None
|
||||||
|
|
||||||
|
# pylint: disable-next=import-outside-toplevel
|
||||||
|
from .helpers.translation import async_get_exception_message
|
||||||
|
|
||||||
|
self._message = async_get_exception_message(
|
||||||
|
self.translation_domain, self.translation_key, self.translation_placeholders
|
||||||
|
)
|
||||||
|
return self._message
|
||||||
|
|
||||||
|
|
||||||
class ConfigValidationError(HomeAssistantError, ExceptionGroup[Exception]):
|
class ConfigValidationError(HomeAssistantError, ExceptionGroup[Exception]):
|
||||||
"""A validation exception occurred when validating the configuration."""
|
"""A validation exception occurred when validating the configuration."""
|
||||||
|
@ -47,10 +81,6 @@ class ConfigValidationError(HomeAssistantError, ExceptionGroup[Exception]):
|
||||||
)
|
)
|
||||||
self._message = message
|
self._message = message
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
"""Return exception message string."""
|
|
||||||
return self._message
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceValidationError(HomeAssistantError):
|
class ServiceValidationError(HomeAssistantError):
|
||||||
"""A validation exception occurred when calling a service."""
|
"""A validation exception occurred when calling a service."""
|
||||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Iterable, Mapping
|
from collections.abc import Iterable, Mapping
|
||||||
|
from contextlib import suppress
|
||||||
import logging
|
import logging
|
||||||
import string
|
import string
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@ -13,7 +14,7 @@ from homeassistant.const import (
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
STATE_UNKNOWN,
|
STATE_UNKNOWN,
|
||||||
)
|
)
|
||||||
from homeassistant.core import Event, HomeAssistant, callback
|
from homeassistant.core import Event, HomeAssistant, async_get_hass, callback
|
||||||
from homeassistant.loader import (
|
from homeassistant.loader import (
|
||||||
Integration,
|
Integration,
|
||||||
async_get_config_flows,
|
async_get_config_flows,
|
||||||
|
@ -528,6 +529,35 @@ def async_translations_loaded(hass: HomeAssistant, components: set[str]) -> bool
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_get_exception_message(
|
||||||
|
translation_domain: str,
|
||||||
|
translation_key: str,
|
||||||
|
translation_placeholders: dict[str, str] | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Return a translated exception message.
|
||||||
|
|
||||||
|
Defaults to English, requires translations to already be cached.
|
||||||
|
"""
|
||||||
|
language = "en"
|
||||||
|
hass = async_get_hass()
|
||||||
|
localize_key = (
|
||||||
|
f"component.{translation_domain}.exceptions.{translation_key}.message"
|
||||||
|
)
|
||||||
|
translations = async_get_cached_translations(hass, language, "exceptions")
|
||||||
|
if localize_key in translations:
|
||||||
|
if message := translations[localize_key]:
|
||||||
|
message = message.rstrip(".")
|
||||||
|
if not translation_placeholders:
|
||||||
|
return message
|
||||||
|
with suppress(KeyError):
|
||||||
|
message = message.format(**translation_placeholders)
|
||||||
|
return message
|
||||||
|
|
||||||
|
# We return the translation key when was not found in the cache
|
||||||
|
return translation_key
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_translate_state(
|
def async_translate_state(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
|
|
@ -300,7 +300,7 @@ async def test_preset_mode_validation(
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
ServiceValidationError,
|
ServiceValidationError,
|
||||||
match="The preset_mode invalid is not a valid preset_mode: home, away",
|
match="Preset mode invalid is not valid. Valid preset modes are: home, away",
|
||||||
) as exc:
|
) as exc:
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
@ -313,13 +313,13 @@ async def test_preset_mode_validation(
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
str(exc.value)
|
str(exc.value)
|
||||||
== "The preset_mode invalid is not a valid preset_mode: home, away"
|
== "Preset mode invalid is not valid. Valid preset modes are: home, away"
|
||||||
)
|
)
|
||||||
assert exc.value.translation_key == "not_valid_preset_mode"
|
assert exc.value.translation_key == "not_valid_preset_mode"
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
ServiceValidationError,
|
ServiceValidationError,
|
||||||
match="The swing_mode invalid is not a valid swing_mode: auto, off",
|
match="Swing mode invalid is not valid. Valid swing modes are: auto, off",
|
||||||
) as exc:
|
) as exc:
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
@ -331,13 +331,14 @@ async def test_preset_mode_validation(
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
str(exc.value) == "The swing_mode invalid is not a valid swing_mode: auto, off"
|
str(exc.value)
|
||||||
|
== "Swing mode invalid is not valid. Valid swing modes are: auto, off"
|
||||||
)
|
)
|
||||||
assert exc.value.translation_key == "not_valid_swing_mode"
|
assert exc.value.translation_key == "not_valid_swing_mode"
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
ServiceValidationError,
|
ServiceValidationError,
|
||||||
match="The fan_mode invalid is not a valid fan_mode: auto, off",
|
match="Fan mode invalid is not valid. Valid fan modes are: auto, off",
|
||||||
) as exc:
|
) as exc:
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
@ -348,7 +349,10 @@ async def test_preset_mode_validation(
|
||||||
},
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
assert str(exc.value) == "The fan_mode invalid is not a valid fan_mode: auto, off"
|
assert (
|
||||||
|
str(exc.value)
|
||||||
|
== "Fan mode invalid is not valid. Valid fan modes are: auto, off"
|
||||||
|
)
|
||||||
assert exc.value.translation_key == "not_valid_fan_mode"
|
assert exc.value.translation_key == "not_valid_fan_mode"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,17 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import (
|
from homeassistant.exceptions import (
|
||||||
ConditionErrorContainer,
|
ConditionErrorContainer,
|
||||||
ConditionErrorIndex,
|
ConditionErrorIndex,
|
||||||
ConditionErrorMessage,
|
ConditionErrorMessage,
|
||||||
|
HomeAssistantError,
|
||||||
TemplateError,
|
TemplateError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -62,3 +67,55 @@ def test_template_message(arg: str | Exception, expected: str) -> None:
|
||||||
"""Ensure we can create TemplateError."""
|
"""Ensure we can create TemplateError."""
|
||||||
template_error = TemplateError(arg)
|
template_error = TemplateError(arg)
|
||||||
assert str(template_error) == expected
|
assert str(template_error) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("exception_args", "exception_kwargs", "args_base_class", "message"),
|
||||||
|
[
|
||||||
|
((), {}, (), ""),
|
||||||
|
(("bla",), {}, ("bla",), "bla"),
|
||||||
|
((None,), {}, (None,), "None"),
|
||||||
|
((type_error_bla := TypeError("bla"),), {}, (type_error_bla,), "bla"),
|
||||||
|
(
|
||||||
|
(),
|
||||||
|
{"translation_domain": "test", "translation_key": "test"},
|
||||||
|
("test",),
|
||||||
|
"test",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
(),
|
||||||
|
{"translation_domain": "test", "translation_key": "bla"},
|
||||||
|
("bla",),
|
||||||
|
"{bla} from cache",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
(),
|
||||||
|
{
|
||||||
|
"translation_domain": "test",
|
||||||
|
"translation_key": "bla",
|
||||||
|
"translation_placeholders": {"bla": "Bla"},
|
||||||
|
},
|
||||||
|
("bla",),
|
||||||
|
"Bla from cache",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_home_assistant_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
exception_args: tuple[Any,],
|
||||||
|
exception_kwargs: dict[str, Any],
|
||||||
|
args_base_class: tuple[Any],
|
||||||
|
message: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test edge cases with HomeAssistantError."""
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.translation.async_get_cached_translations",
|
||||||
|
return_value={"component.test.exceptions.bla.message": "{bla} from cache"},
|
||||||
|
):
|
||||||
|
with pytest.raises(HomeAssistantError) as exc:
|
||||||
|
raise HomeAssistantError(*exception_args, **exception_kwargs)
|
||||||
|
assert exc.value.args == args_base_class
|
||||||
|
assert str(exc.value) == message
|
||||||
|
# Get string of exception again from the cache
|
||||||
|
assert str(exc.value) == message
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue