mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add guard to HomeAssistantError __str__
method to prevent a recursive loop (#113913)
* Add guard to HomeAssistantError `__str__` method to prevent a recursive loop * Use repr of class instance instead * Apply suggestion to explain __str__ method is missing --------- Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
a6d98c1857
commit
4da701a8e9
@ -64,6 +64,15 @@ class HomeAssistantError(Exception):
|
|||||||
return self._message
|
return self._message
|
||||||
|
|
||||||
if not self.generate_message:
|
if not self.generate_message:
|
||||||
|
# Initialize self._message to the string repr of the class
|
||||||
|
# to prevent a recursive loop.
|
||||||
|
self._message = (
|
||||||
|
f"Parent class {self.__class__.__name__} is missing __str__ method"
|
||||||
|
)
|
||||||
|
# If the there is an other super class involved,
|
||||||
|
# we want to call its __str__ method.
|
||||||
|
# If the super().__str__ method is missing in the base_class
|
||||||
|
# the call will be recursive and we return our initialized default.
|
||||||
self._message = super().__str__()
|
self._message = super().__str__()
|
||||||
return self._message
|
return self._message
|
||||||
|
|
||||||
|
@ -119,3 +119,159 @@ async def test_home_assistant_error(
|
|||||||
assert str(exc.value) == message
|
assert str(exc.value) == message
|
||||||
# Get string of exception again from the cache
|
# Get string of exception again from the cache
|
||||||
assert str(exc.value) == message
|
assert str(exc.value) == message
|
||||||
|
|
||||||
|
|
||||||
|
async def test_home_assistant_error_subclass(hass: HomeAssistant) -> None:
|
||||||
|
"""Test __str__ method on an HomeAssistantError subclass."""
|
||||||
|
|
||||||
|
class _SubExceptionDefault(HomeAssistantError):
|
||||||
|
"""Sub class, default with generated message."""
|
||||||
|
|
||||||
|
class _SubExceptionConstructor(HomeAssistantError):
|
||||||
|
"""Sub class with constructor, no generated message."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
custom_arg: str,
|
||||||
|
translation_domain: str | None = None,
|
||||||
|
translation_key: str | None = None,
|
||||||
|
translation_placeholders: dict[str, str] | None = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(
|
||||||
|
self,
|
||||||
|
translation_domain=translation_domain,
|
||||||
|
translation_key=translation_key,
|
||||||
|
translation_placeholders=translation_placeholders,
|
||||||
|
)
|
||||||
|
self.custom_arg = custom_arg
|
||||||
|
|
||||||
|
class _SubExceptionConstructorGenerate(HomeAssistantError):
|
||||||
|
"""Sub class with constructor, with generated message."""
|
||||||
|
|
||||||
|
generate_message: bool = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
custom_arg: str,
|
||||||
|
translation_domain: str | None = None,
|
||||||
|
translation_key: str | None = None,
|
||||||
|
translation_placeholders: dict[str, str] | None = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(
|
||||||
|
self,
|
||||||
|
translation_domain=translation_domain,
|
||||||
|
translation_key=translation_key,
|
||||||
|
translation_placeholders=translation_placeholders,
|
||||||
|
)
|
||||||
|
self.custom_arg = custom_arg
|
||||||
|
|
||||||
|
class _SubExceptionGenerate(HomeAssistantError):
|
||||||
|
"""Sub class, no generated message."""
|
||||||
|
|
||||||
|
generate_message: bool = True
|
||||||
|
|
||||||
|
class _SubClassWithExceptionGroup(HomeAssistantError, BaseExceptionGroup):
|
||||||
|
"""Sub class with exception group, no generated message."""
|
||||||
|
|
||||||
|
class _SubClassWithExceptionGroupGenerate(HomeAssistantError, BaseExceptionGroup):
|
||||||
|
"""Sub class with exception group and generated message."""
|
||||||
|
|
||||||
|
generate_message: bool = True
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.translation.async_get_cached_translations",
|
||||||
|
return_value={"component.test.exceptions.bla.message": "{bla} from cache"},
|
||||||
|
):
|
||||||
|
# A subclass without a constructor generates a message by default
|
||||||
|
with pytest.raises(HomeAssistantError) as exc:
|
||||||
|
raise _SubExceptionDefault(
|
||||||
|
translation_domain="test",
|
||||||
|
translation_key="bla",
|
||||||
|
translation_placeholders={"bla": "Bla"},
|
||||||
|
)
|
||||||
|
assert str(exc.value) == "Bla from cache"
|
||||||
|
|
||||||
|
# A subclass with a constructor that does not parse `args` to the super class
|
||||||
|
with pytest.raises(HomeAssistantError) as exc:
|
||||||
|
raise _SubExceptionConstructor(
|
||||||
|
"custom arg",
|
||||||
|
translation_domain="test",
|
||||||
|
translation_key="bla",
|
||||||
|
translation_placeholders={"bla": "Bla"},
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
str(exc.value)
|
||||||
|
== "Parent class _SubExceptionConstructor is missing __str__ method"
|
||||||
|
)
|
||||||
|
with pytest.raises(HomeAssistantError) as exc:
|
||||||
|
raise _SubExceptionConstructor(
|
||||||
|
"custom arg",
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
str(exc.value)
|
||||||
|
== "Parent class _SubExceptionConstructor is missing __str__ method"
|
||||||
|
)
|
||||||
|
|
||||||
|
# A subclass with a constructor that generates the message
|
||||||
|
with pytest.raises(HomeAssistantError) as exc:
|
||||||
|
raise _SubExceptionConstructorGenerate(
|
||||||
|
"custom arg",
|
||||||
|
translation_domain="test",
|
||||||
|
translation_key="bla",
|
||||||
|
translation_placeholders={"bla": "Bla"},
|
||||||
|
)
|
||||||
|
assert str(exc.value) == "Bla from cache"
|
||||||
|
|
||||||
|
# A subclass without overridden constructors and passed args
|
||||||
|
# defaults to the passed args
|
||||||
|
with pytest.raises(HomeAssistantError) as exc:
|
||||||
|
raise _SubExceptionDefault(
|
||||||
|
ValueError("wrong value"),
|
||||||
|
translation_domain="test",
|
||||||
|
translation_key="bla",
|
||||||
|
translation_placeholders={"bla": "Bla"},
|
||||||
|
)
|
||||||
|
assert str(exc.value) == "wrong value"
|
||||||
|
|
||||||
|
# A subclass without overridden constructors and passed args
|
||||||
|
# and generate_message = True, generates a message
|
||||||
|
with pytest.raises(HomeAssistantError) as exc:
|
||||||
|
raise _SubExceptionGenerate(
|
||||||
|
ValueError("wrong value"),
|
||||||
|
translation_domain="test",
|
||||||
|
translation_key="bla",
|
||||||
|
translation_placeholders={"bla": "Bla"},
|
||||||
|
)
|
||||||
|
assert str(exc.value) == "Bla from cache"
|
||||||
|
|
||||||
|
# A subclass with and ExceptionGroup subclass requires a message to be passed.
|
||||||
|
# As we pass args, we will not generate the message.
|
||||||
|
# The __str__ constructor defaults to that of the super class.
|
||||||
|
with pytest.raises(HomeAssistantError) as exc:
|
||||||
|
raise _SubClassWithExceptionGroup(
|
||||||
|
"group message",
|
||||||
|
[ValueError("wrong value"), TypeError("wrong type")],
|
||||||
|
translation_domain="test",
|
||||||
|
translation_key="bla",
|
||||||
|
translation_placeholders={"bla": "Bla"},
|
||||||
|
)
|
||||||
|
assert str(exc.value) == "group message (2 sub-exceptions)"
|
||||||
|
with pytest.raises(HomeAssistantError) as exc:
|
||||||
|
raise _SubClassWithExceptionGroup(
|
||||||
|
"group message",
|
||||||
|
[ValueError("wrong value"), TypeError("wrong type")],
|
||||||
|
)
|
||||||
|
assert str(exc.value) == "group message (2 sub-exceptions)"
|
||||||
|
|
||||||
|
# A subclass with and ExceptionGroup subclass requires a message to be passed.
|
||||||
|
# The `generate_message` flag is set.`
|
||||||
|
# The __str__ constructor will return the generated message.
|
||||||
|
with pytest.raises(HomeAssistantError) as exc:
|
||||||
|
raise _SubClassWithExceptionGroupGenerate(
|
||||||
|
"group message",
|
||||||
|
[ValueError("wrong value"), TypeError("wrong type")],
|
||||||
|
translation_domain="test",
|
||||||
|
translation_key="bla",
|
||||||
|
translation_placeholders={"bla": "Bla"},
|
||||||
|
)
|
||||||
|
assert str(exc.value) == "Bla from cache"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user