Allow specifying a custom log function for template render (#99572)
* Allow specifying a custom log function for template render * Bypass template cache when reporting errors + fix tests * Send errors as events * Fix logic for creating new TemplateEnvironment * Add strict mode back * Only send error events if report_errors is True * Force test of websocket_api only * Debug test * Run pytest with higher verbosity * Timeout after 1 minute, enable syslog output * Adjust timeout * Add debug logs * Fix unsafe call to WebSocketHandler._send_message * Remove debug code * Improve test coverage * Revert accidental change * Include severity in error events * Remove redundant information from error events
This commit is contained in:
parent
f41b045244
commit
48f7924e9e
5 changed files with 374 additions and 84 deletions
|
@ -5,6 +5,7 @@ from collections.abc import Callable
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
from functools import lru_cache, partial
|
from functools import lru_cache, partial
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@ -505,6 +506,7 @@ def _cached_template(template_str: str, hass: HomeAssistant) -> template.Templat
|
||||||
vol.Optional("variables"): dict,
|
vol.Optional("variables"): dict,
|
||||||
vol.Optional("timeout"): vol.Coerce(float),
|
vol.Optional("timeout"): vol.Coerce(float),
|
||||||
vol.Optional("strict", default=False): bool,
|
vol.Optional("strict", default=False): bool,
|
||||||
|
vol.Optional("report_errors", default=False): bool,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@decorators.async_response
|
@decorators.async_response
|
||||||
|
@ -513,14 +515,32 @@ async def handle_render_template(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle render_template command."""
|
"""Handle render_template command."""
|
||||||
template_str = msg["template"]
|
template_str = msg["template"]
|
||||||
template_obj = _cached_template(template_str, hass)
|
report_errors: bool = msg["report_errors"]
|
||||||
|
if report_errors:
|
||||||
|
template_obj = template.Template(template_str, hass)
|
||||||
|
else:
|
||||||
|
template_obj = _cached_template(template_str, hass)
|
||||||
variables = msg.get("variables")
|
variables = msg.get("variables")
|
||||||
timeout = msg.get("timeout")
|
timeout = msg.get("timeout")
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _error_listener(level: int, template_error: str) -> None:
|
||||||
|
connection.send_message(
|
||||||
|
messages.event_message(
|
||||||
|
msg["id"],
|
||||||
|
{"error": template_error, "level": logging.getLevelName(level)},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _thread_safe_error_listener(level: int, template_error: str) -> None:
|
||||||
|
hass.loop.call_soon_threadsafe(_error_listener, level, template_error)
|
||||||
|
|
||||||
if timeout:
|
if timeout:
|
||||||
try:
|
try:
|
||||||
|
log_fn = _thread_safe_error_listener if report_errors else None
|
||||||
timed_out = await template_obj.async_render_will_timeout(
|
timed_out = await template_obj.async_render_will_timeout(
|
||||||
timeout, variables, strict=msg["strict"]
|
timeout, variables, strict=msg["strict"], log_fn=log_fn
|
||||||
)
|
)
|
||||||
except TemplateError as ex:
|
except TemplateError as ex:
|
||||||
connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex))
|
connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex))
|
||||||
|
@ -542,7 +562,11 @@ async def handle_render_template(
|
||||||
track_template_result = updates.pop()
|
track_template_result = updates.pop()
|
||||||
result = track_template_result.result
|
result = track_template_result.result
|
||||||
if isinstance(result, TemplateError):
|
if isinstance(result, TemplateError):
|
||||||
connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(result))
|
if not report_errors:
|
||||||
|
return
|
||||||
|
connection.send_message(
|
||||||
|
messages.event_message(msg["id"], {"error": str(result)})
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
connection.send_message(
|
connection.send_message(
|
||||||
|
@ -552,12 +576,14 @@ async def handle_render_template(
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
log_fn = _error_listener if report_errors else None
|
||||||
info = async_track_template_result(
|
info = async_track_template_result(
|
||||||
hass,
|
hass,
|
||||||
[TrackTemplate(template_obj, variables)],
|
[TrackTemplate(template_obj, variables)],
|
||||||
_template_listener,
|
_template_listener,
|
||||||
raise_on_template_error=True,
|
raise_on_template_error=True,
|
||||||
strict=msg["strict"],
|
strict=msg["strict"],
|
||||||
|
log_fn=log_fn,
|
||||||
)
|
)
|
||||||
except TemplateError as ex:
|
except TemplateError as ex:
|
||||||
connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex))
|
connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex))
|
||||||
|
|
|
@ -915,7 +915,12 @@ class TrackTemplateResultInfo:
|
||||||
"""Return the representation."""
|
"""Return the representation."""
|
||||||
return f"<TrackTemplateResultInfo {self._info}>"
|
return f"<TrackTemplateResultInfo {self._info}>"
|
||||||
|
|
||||||
def async_setup(self, raise_on_template_error: bool, strict: bool = False) -> None:
|
def async_setup(
|
||||||
|
self,
|
||||||
|
raise_on_template_error: bool,
|
||||||
|
strict: bool = False,
|
||||||
|
log_fn: Callable[[int, str], None] | None = None,
|
||||||
|
) -> None:
|
||||||
"""Activation of template tracking."""
|
"""Activation of template tracking."""
|
||||||
block_render = False
|
block_render = False
|
||||||
super_template = self._track_templates[0] if self._has_super_template else None
|
super_template = self._track_templates[0] if self._has_super_template else None
|
||||||
|
@ -925,7 +930,7 @@ class TrackTemplateResultInfo:
|
||||||
template = super_template.template
|
template = super_template.template
|
||||||
variables = super_template.variables
|
variables = super_template.variables
|
||||||
self._info[template] = info = template.async_render_to_info(
|
self._info[template] = info = template.async_render_to_info(
|
||||||
variables, strict=strict
|
variables, strict=strict, log_fn=log_fn
|
||||||
)
|
)
|
||||||
|
|
||||||
# If the super template did not render to True, don't update other templates
|
# If the super template did not render to True, don't update other templates
|
||||||
|
@ -946,7 +951,7 @@ class TrackTemplateResultInfo:
|
||||||
template = track_template_.template
|
template = track_template_.template
|
||||||
variables = track_template_.variables
|
variables = track_template_.variables
|
||||||
self._info[template] = info = template.async_render_to_info(
|
self._info[template] = info = template.async_render_to_info(
|
||||||
variables, strict=strict
|
variables, strict=strict, log_fn=log_fn
|
||||||
)
|
)
|
||||||
|
|
||||||
if info.exception:
|
if info.exception:
|
||||||
|
@ -1233,6 +1238,7 @@ def async_track_template_result(
|
||||||
action: TrackTemplateResultListener,
|
action: TrackTemplateResultListener,
|
||||||
raise_on_template_error: bool = False,
|
raise_on_template_error: bool = False,
|
||||||
strict: bool = False,
|
strict: bool = False,
|
||||||
|
log_fn: Callable[[int, str], None] | None = None,
|
||||||
has_super_template: bool = False,
|
has_super_template: bool = False,
|
||||||
) -> TrackTemplateResultInfo:
|
) -> TrackTemplateResultInfo:
|
||||||
"""Add a listener that fires when the result of a template changes.
|
"""Add a listener that fires when the result of a template changes.
|
||||||
|
@ -1264,6 +1270,9 @@ def async_track_template_result(
|
||||||
tracking.
|
tracking.
|
||||||
strict
|
strict
|
||||||
When set to True, raise on undefined variables.
|
When set to True, raise on undefined variables.
|
||||||
|
log_fn
|
||||||
|
If not None, template error messages will logging by calling log_fn
|
||||||
|
instead of the normal logging facility.
|
||||||
has_super_template
|
has_super_template
|
||||||
When set to True, the first template will block rendering of other
|
When set to True, the first template will block rendering of other
|
||||||
templates if it doesn't render as True.
|
templates if it doesn't render as True.
|
||||||
|
@ -1274,7 +1283,7 @@ def async_track_template_result(
|
||||||
|
|
||||||
"""
|
"""
|
||||||
tracker = TrackTemplateResultInfo(hass, track_templates, action, has_super_template)
|
tracker = TrackTemplateResultInfo(hass, track_templates, action, has_super_template)
|
||||||
tracker.async_setup(raise_on_template_error, strict=strict)
|
tracker.async_setup(raise_on_template_error, strict=strict, log_fn=log_fn)
|
||||||
return tracker
|
return tracker
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -458,6 +458,7 @@ class Template:
|
||||||
"_exc_info",
|
"_exc_info",
|
||||||
"_limited",
|
"_limited",
|
||||||
"_strict",
|
"_strict",
|
||||||
|
"_log_fn",
|
||||||
"_hash_cache",
|
"_hash_cache",
|
||||||
"_renders",
|
"_renders",
|
||||||
)
|
)
|
||||||
|
@ -475,6 +476,7 @@ class Template:
|
||||||
self._exc_info: sys._OptExcInfo | None = None
|
self._exc_info: sys._OptExcInfo | None = None
|
||||||
self._limited: bool | None = None
|
self._limited: bool | None = None
|
||||||
self._strict: bool | None = None
|
self._strict: bool | None = None
|
||||||
|
self._log_fn: Callable[[int, str], None] | None = None
|
||||||
self._hash_cache: int = hash(self.template)
|
self._hash_cache: int = hash(self.template)
|
||||||
self._renders: int = 0
|
self._renders: int = 0
|
||||||
|
|
||||||
|
@ -482,6 +484,11 @@ class Template:
|
||||||
def _env(self) -> TemplateEnvironment:
|
def _env(self) -> TemplateEnvironment:
|
||||||
if self.hass is None:
|
if self.hass is None:
|
||||||
return _NO_HASS_ENV
|
return _NO_HASS_ENV
|
||||||
|
# Bypass cache if a custom log function is specified
|
||||||
|
if self._log_fn is not None:
|
||||||
|
return TemplateEnvironment(
|
||||||
|
self.hass, self._limited, self._strict, self._log_fn
|
||||||
|
)
|
||||||
if self._limited:
|
if self._limited:
|
||||||
wanted_env = _ENVIRONMENT_LIMITED
|
wanted_env = _ENVIRONMENT_LIMITED
|
||||||
elif self._strict:
|
elif self._strict:
|
||||||
|
@ -491,9 +498,7 @@ class Template:
|
||||||
ret: TemplateEnvironment | None = self.hass.data.get(wanted_env)
|
ret: TemplateEnvironment | None = self.hass.data.get(wanted_env)
|
||||||
if ret is None:
|
if ret is None:
|
||||||
ret = self.hass.data[wanted_env] = TemplateEnvironment(
|
ret = self.hass.data[wanted_env] = TemplateEnvironment(
|
||||||
self.hass,
|
self.hass, self._limited, self._strict, self._log_fn
|
||||||
self._limited,
|
|
||||||
self._strict,
|
|
||||||
)
|
)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
@ -537,6 +542,7 @@ class Template:
|
||||||
parse_result: bool = True,
|
parse_result: bool = True,
|
||||||
limited: bool = False,
|
limited: bool = False,
|
||||||
strict: bool = False,
|
strict: bool = False,
|
||||||
|
log_fn: Callable[[int, str], None] | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Render given template.
|
"""Render given template.
|
||||||
|
@ -553,7 +559,7 @@ class Template:
|
||||||
return self.template
|
return self.template
|
||||||
return self._parse_result(self.template)
|
return self._parse_result(self.template)
|
||||||
|
|
||||||
compiled = self._compiled or self._ensure_compiled(limited, strict)
|
compiled = self._compiled or self._ensure_compiled(limited, strict, log_fn)
|
||||||
|
|
||||||
if variables is not None:
|
if variables is not None:
|
||||||
kwargs.update(variables)
|
kwargs.update(variables)
|
||||||
|
@ -608,6 +614,7 @@ class Template:
|
||||||
timeout: float,
|
timeout: float,
|
||||||
variables: TemplateVarsType = None,
|
variables: TemplateVarsType = None,
|
||||||
strict: bool = False,
|
strict: bool = False,
|
||||||
|
log_fn: Callable[[int, str], None] | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Check to see if rendering a template will timeout during render.
|
"""Check to see if rendering a template will timeout during render.
|
||||||
|
@ -628,7 +635,7 @@ class Template:
|
||||||
if self.is_static:
|
if self.is_static:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
compiled = self._compiled or self._ensure_compiled(strict=strict)
|
compiled = self._compiled or self._ensure_compiled(strict=strict, log_fn=log_fn)
|
||||||
|
|
||||||
if variables is not None:
|
if variables is not None:
|
||||||
kwargs.update(variables)
|
kwargs.update(variables)
|
||||||
|
@ -664,7 +671,11 @@ class Template:
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_render_to_info(
|
def async_render_to_info(
|
||||||
self, variables: TemplateVarsType = None, strict: bool = False, **kwargs: Any
|
self,
|
||||||
|
variables: TemplateVarsType = None,
|
||||||
|
strict: bool = False,
|
||||||
|
log_fn: Callable[[int, str], None] | None = None,
|
||||||
|
**kwargs: Any,
|
||||||
) -> RenderInfo:
|
) -> RenderInfo:
|
||||||
"""Render the template and collect an entity filter."""
|
"""Render the template and collect an entity filter."""
|
||||||
self._renders += 1
|
self._renders += 1
|
||||||
|
@ -680,7 +691,9 @@ class Template:
|
||||||
|
|
||||||
token = _render_info.set(render_info)
|
token = _render_info.set(render_info)
|
||||||
try:
|
try:
|
||||||
render_info._result = self.async_render(variables, strict=strict, **kwargs)
|
render_info._result = self.async_render(
|
||||||
|
variables, strict=strict, log_fn=log_fn, **kwargs
|
||||||
|
)
|
||||||
except TemplateError as ex:
|
except TemplateError as ex:
|
||||||
render_info.exception = ex
|
render_info.exception = ex
|
||||||
finally:
|
finally:
|
||||||
|
@ -743,7 +756,10 @@ class Template:
|
||||||
return value if error_value is _SENTINEL else error_value
|
return value if error_value is _SENTINEL else error_value
|
||||||
|
|
||||||
def _ensure_compiled(
|
def _ensure_compiled(
|
||||||
self, limited: bool = False, strict: bool = False
|
self,
|
||||||
|
limited: bool = False,
|
||||||
|
strict: bool = False,
|
||||||
|
log_fn: Callable[[int, str], None] | None = None,
|
||||||
) -> jinja2.Template:
|
) -> jinja2.Template:
|
||||||
"""Bind a template to a specific hass instance."""
|
"""Bind a template to a specific hass instance."""
|
||||||
self.ensure_valid()
|
self.ensure_valid()
|
||||||
|
@ -756,10 +772,14 @@ class Template:
|
||||||
self._strict is None or self._strict == strict
|
self._strict is None or self._strict == strict
|
||||||
), "can't change between strict and non strict template"
|
), "can't change between strict and non strict template"
|
||||||
assert not (strict and limited), "can't combine strict and limited template"
|
assert not (strict and limited), "can't combine strict and limited template"
|
||||||
|
assert (
|
||||||
|
self._log_fn is None or self._log_fn == log_fn
|
||||||
|
), "can't change custom log function"
|
||||||
assert self._compiled_code is not None, "template code was not compiled"
|
assert self._compiled_code is not None, "template code was not compiled"
|
||||||
|
|
||||||
self._limited = limited
|
self._limited = limited
|
||||||
self._strict = strict
|
self._strict = strict
|
||||||
|
self._log_fn = log_fn
|
||||||
env = self._env
|
env = self._env
|
||||||
|
|
||||||
self._compiled = jinja2.Template.from_code(
|
self._compiled = jinja2.Template.from_code(
|
||||||
|
@ -2178,45 +2198,56 @@ def _render_with_context(
|
||||||
return template.render(**kwargs)
|
return template.render(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class LoggingUndefined(jinja2.Undefined):
|
def make_logging_undefined(
|
||||||
|
strict: bool | None, log_fn: Callable[[int, str], None] | None
|
||||||
|
) -> type[jinja2.Undefined]:
|
||||||
"""Log on undefined variables."""
|
"""Log on undefined variables."""
|
||||||
|
|
||||||
def _log_message(self) -> None:
|
if strict:
|
||||||
|
return jinja2.StrictUndefined
|
||||||
|
|
||||||
|
def _log_with_logger(level: int, msg: str) -> None:
|
||||||
template, action = template_cv.get() or ("", "rendering or compiling")
|
template, action = template_cv.get() or ("", "rendering or compiling")
|
||||||
_LOGGER.warning(
|
_LOGGER.log(
|
||||||
"Template variable warning: %s when %s '%s'",
|
level,
|
||||||
self._undefined_message,
|
"Template variable %s: %s when %s '%s'",
|
||||||
|
logging.getLevelName(level).lower(),
|
||||||
|
msg,
|
||||||
action,
|
action,
|
||||||
template,
|
template,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _fail_with_undefined_error(self, *args, **kwargs):
|
_log_fn = log_fn or _log_with_logger
|
||||||
try:
|
|
||||||
return super()._fail_with_undefined_error(*args, **kwargs)
|
|
||||||
except self._undefined_exception as ex:
|
|
||||||
template, action = template_cv.get() or ("", "rendering or compiling")
|
|
||||||
_LOGGER.error(
|
|
||||||
"Template variable error: %s when %s '%s'",
|
|
||||||
self._undefined_message,
|
|
||||||
action,
|
|
||||||
template,
|
|
||||||
)
|
|
||||||
raise ex
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
class LoggingUndefined(jinja2.Undefined):
|
||||||
"""Log undefined __str___."""
|
"""Log on undefined variables."""
|
||||||
self._log_message()
|
|
||||||
return super().__str__()
|
|
||||||
|
|
||||||
def __iter__(self):
|
def _log_message(self) -> None:
|
||||||
"""Log undefined __iter___."""
|
_log_fn(logging.WARNING, self._undefined_message)
|
||||||
self._log_message()
|
|
||||||
return super().__iter__()
|
|
||||||
|
|
||||||
def __bool__(self) -> bool:
|
def _fail_with_undefined_error(self, *args, **kwargs):
|
||||||
"""Log undefined __bool___."""
|
try:
|
||||||
self._log_message()
|
return super()._fail_with_undefined_error(*args, **kwargs)
|
||||||
return super().__bool__()
|
except self._undefined_exception as ex:
|
||||||
|
_log_fn(logging.ERROR, self._undefined_message)
|
||||||
|
raise ex
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Log undefined __str___."""
|
||||||
|
self._log_message()
|
||||||
|
return super().__str__()
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""Log undefined __iter___."""
|
||||||
|
self._log_message()
|
||||||
|
return super().__iter__()
|
||||||
|
|
||||||
|
def __bool__(self) -> bool:
|
||||||
|
"""Log undefined __bool___."""
|
||||||
|
self._log_message()
|
||||||
|
return super().__bool__()
|
||||||
|
|
||||||
|
return LoggingUndefined
|
||||||
|
|
||||||
|
|
||||||
async def async_load_custom_templates(hass: HomeAssistant) -> None:
|
async def async_load_custom_templates(hass: HomeAssistant) -> None:
|
||||||
|
@ -2281,14 +2312,10 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||||
hass: HomeAssistant | None,
|
hass: HomeAssistant | None,
|
||||||
limited: bool | None = False,
|
limited: bool | None = False,
|
||||||
strict: bool | None = False,
|
strict: bool | None = False,
|
||||||
|
log_fn: Callable[[int, str], None] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialise template environment."""
|
"""Initialise template environment."""
|
||||||
undefined: type[LoggingUndefined] | type[jinja2.StrictUndefined]
|
super().__init__(undefined=make_logging_undefined(strict, log_fn))
|
||||||
if not strict:
|
|
||||||
undefined = LoggingUndefined
|
|
||||||
else:
|
|
||||||
undefined = jinja2.StrictUndefined
|
|
||||||
super().__init__(undefined=undefined)
|
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.template_cache: weakref.WeakValueDictionary[
|
self.template_cache: weakref.WeakValueDictionary[
|
||||||
str | jinja2.nodes.Template, CodeType | str | None
|
str | jinja2.nodes.Template, CodeType | str | None
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
import datetime
|
import datetime
|
||||||
|
import logging
|
||||||
from unittest.mock import ANY, AsyncMock, Mock, patch
|
from unittest.mock import ANY, AsyncMock, Mock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -33,7 +34,11 @@ from tests.common import (
|
||||||
async_mock_service,
|
async_mock_service,
|
||||||
mock_platform,
|
mock_platform,
|
||||||
)
|
)
|
||||||
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
from tests.typing import (
|
||||||
|
ClientSessionGenerator,
|
||||||
|
MockHAClientWebSocket,
|
||||||
|
WebSocketGenerator,
|
||||||
|
)
|
||||||
|
|
||||||
STATE_KEY_SHORT_NAMES = {
|
STATE_KEY_SHORT_NAMES = {
|
||||||
"entity_id": "e",
|
"entity_id": "e",
|
||||||
|
@ -1225,46 +1230,187 @@ async def test_render_template_manual_entity_ids_no_longer_needed(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
EMPTY_LISTENERS = {"all": False, "entities": [], "domains": [], "time": False}
|
||||||
|
|
||||||
|
ERR_MSG = {"type": "result", "success": False}
|
||||||
|
|
||||||
|
VARIABLE_ERROR_UNDEFINED_FUNC = {
|
||||||
|
"error": "'my_unknown_func' is undefined",
|
||||||
|
"level": "ERROR",
|
||||||
|
}
|
||||||
|
TEMPLATE_ERROR_UNDEFINED_FUNC = {
|
||||||
|
"code": "template_error",
|
||||||
|
"message": "UndefinedError: 'my_unknown_func' is undefined",
|
||||||
|
}
|
||||||
|
|
||||||
|
VARIABLE_WARNING_UNDEFINED_VAR = {
|
||||||
|
"error": "'my_unknown_var' is undefined",
|
||||||
|
"level": "WARNING",
|
||||||
|
}
|
||||||
|
TEMPLATE_ERROR_UNDEFINED_VAR = {
|
||||||
|
"code": "template_error",
|
||||||
|
"message": "UndefinedError: 'my_unknown_var' is undefined",
|
||||||
|
}
|
||||||
|
|
||||||
|
TEMPLATE_ERROR_UNDEFINED_FILTER = {
|
||||||
|
"code": "template_error",
|
||||||
|
"message": "TemplateAssertionError: No filter named 'unknown_filter'.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"template",
|
("template", "expected_events"),
|
||||||
[
|
[
|
||||||
"{{ my_unknown_func() + 1 }}",
|
(
|
||||||
"{{ my_unknown_var }}",
|
"{{ my_unknown_func() + 1 }}",
|
||||||
"{{ my_unknown_var + 1 }}",
|
[
|
||||||
"{{ now() | unknown_filter }}",
|
{"type": "event", "event": VARIABLE_ERROR_UNDEFINED_FUNC},
|
||||||
|
ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"{{ my_unknown_var }}",
|
||||||
|
[
|
||||||
|
{"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR},
|
||||||
|
{"type": "result", "success": True, "result": None},
|
||||||
|
{"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"event": {"result": "", "listeners": EMPTY_LISTENERS},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"{{ my_unknown_var + 1 }}",
|
||||||
|
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"{{ now() | unknown_filter }}",
|
||||||
|
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_render_template_with_error(
|
async def test_render_template_with_error(
|
||||||
hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture, template
|
hass: HomeAssistant,
|
||||||
|
websocket_client: MockHAClientWebSocket,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
template: str,
|
||||||
|
expected_events: list[dict[str, str]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test a template with an error."""
|
"""Test a template with an error."""
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
await websocket_client.send_json(
|
await websocket_client.send_json(
|
||||||
{"id": 5, "type": "render_template", "template": template, "strict": True}
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "render_template",
|
||||||
|
"template": template,
|
||||||
|
"report_errors": True,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
msg = await websocket_client.receive_json()
|
for expected_event in expected_events:
|
||||||
assert msg["id"] == 5
|
msg = await websocket_client.receive_json()
|
||||||
assert msg["type"] == const.TYPE_RESULT
|
assert msg["id"] == 5
|
||||||
assert not msg["success"]
|
for key, value in expected_event.items():
|
||||||
assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR
|
assert msg[key] == value
|
||||||
|
|
||||||
assert "Template variable error" not in caplog.text
|
assert "Template variable error" not in caplog.text
|
||||||
|
assert "Template variable warning" not in caplog.text
|
||||||
assert "TemplateError" not in caplog.text
|
assert "TemplateError" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"template",
|
("template", "expected_events"),
|
||||||
[
|
[
|
||||||
"{{ my_unknown_func() + 1 }}",
|
(
|
||||||
"{{ my_unknown_var }}",
|
"{{ my_unknown_func() + 1 }}",
|
||||||
"{{ my_unknown_var + 1 }}",
|
[
|
||||||
"{{ now() | unknown_filter }}",
|
{"type": "event", "event": VARIABLE_ERROR_UNDEFINED_FUNC},
|
||||||
|
ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"{{ my_unknown_var }}",
|
||||||
|
[
|
||||||
|
{"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR},
|
||||||
|
{"type": "result", "success": True, "result": None},
|
||||||
|
{"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"event": {"result": "", "listeners": EMPTY_LISTENERS},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"{{ my_unknown_var + 1 }}",
|
||||||
|
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"{{ now() | unknown_filter }}",
|
||||||
|
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_render_template_with_timeout_and_error(
|
async def test_render_template_with_timeout_and_error(
|
||||||
hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture, template
|
hass: HomeAssistant,
|
||||||
|
websocket_client,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
template: str,
|
||||||
|
expected_events: list[dict[str, str]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test a template with an error with a timeout."""
|
"""Test a template with an error with a timeout."""
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
await websocket_client.send_json(
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "render_template",
|
||||||
|
"template": template,
|
||||||
|
"timeout": 5,
|
||||||
|
"report_errors": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for expected_event in expected_events:
|
||||||
|
msg = await websocket_client.receive_json()
|
||||||
|
assert msg["id"] == 5
|
||||||
|
for key, value in expected_event.items():
|
||||||
|
assert msg[key] == value
|
||||||
|
|
||||||
|
assert "Template variable error" not in caplog.text
|
||||||
|
assert "Template variable warning" not in caplog.text
|
||||||
|
assert "TemplateError" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("template", "expected_events"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"{{ my_unknown_func() + 1 }}",
|
||||||
|
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC}],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"{{ my_unknown_var }}",
|
||||||
|
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"{{ my_unknown_var + 1 }}",
|
||||||
|
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"{{ now() | unknown_filter }}",
|
||||||
|
[ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_render_template_strict_with_timeout_and_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
websocket_client,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
template: str,
|
||||||
|
expected_events: list[dict[str, str]],
|
||||||
|
) -> None:
|
||||||
|
"""Test a template with an error with a timeout."""
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
await websocket_client.send_json(
|
await websocket_client.send_json(
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
|
@ -1275,13 +1421,14 @@ async def test_render_template_with_timeout_and_error(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
msg = await websocket_client.receive_json()
|
for expected_event in expected_events:
|
||||||
assert msg["id"] == 5
|
msg = await websocket_client.receive_json()
|
||||||
assert msg["type"] == const.TYPE_RESULT
|
assert msg["id"] == 5
|
||||||
assert not msg["success"]
|
for key, value in expected_event.items():
|
||||||
assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR
|
assert msg[key] == value
|
||||||
|
|
||||||
assert "Template variable error" not in caplog.text
|
assert "Template variable error" not in caplog.text
|
||||||
|
assert "Template variable warning" not in caplog.text
|
||||||
assert "TemplateError" not in caplog.text
|
assert "TemplateError" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@ -1299,13 +1446,19 @@ async def test_render_template_error_in_template_code(
|
||||||
assert not msg["success"]
|
assert not msg["success"]
|
||||||
assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR
|
assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR
|
||||||
|
|
||||||
|
assert "Template variable error" not in caplog.text
|
||||||
|
assert "Template variable warning" not in caplog.text
|
||||||
assert "TemplateError" not in caplog.text
|
assert "TemplateError" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
async def test_render_template_with_delayed_error(
|
async def test_render_template_with_delayed_error(
|
||||||
hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture
|
hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test a template with an error that only happens after a state change."""
|
"""Test a template with an error that only happens after a state change.
|
||||||
|
|
||||||
|
In this test report_errors is enabled.
|
||||||
|
"""
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
hass.states.async_set("sensor.test", "on")
|
hass.states.async_set("sensor.test", "on")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
@ -1318,12 +1471,16 @@ async def test_render_template_with_delayed_error(
|
||||||
"""
|
"""
|
||||||
|
|
||||||
await websocket_client.send_json(
|
await websocket_client.send_json(
|
||||||
{"id": 5, "type": "render_template", "template": template_str}
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "render_template",
|
||||||
|
"template": template_str,
|
||||||
|
"report_errors": True,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
msg = await websocket_client.receive_json()
|
msg = await websocket_client.receive_json()
|
||||||
|
|
||||||
assert msg["id"] == 5
|
assert msg["id"] == 5
|
||||||
assert msg["type"] == const.TYPE_RESULT
|
assert msg["type"] == const.TYPE_RESULT
|
||||||
assert msg["success"]
|
assert msg["success"]
|
||||||
|
@ -1347,13 +1504,74 @@ async def test_render_template_with_delayed_error(
|
||||||
|
|
||||||
msg = await websocket_client.receive_json()
|
msg = await websocket_client.receive_json()
|
||||||
assert msg["id"] == 5
|
assert msg["id"] == 5
|
||||||
assert msg["type"] == const.TYPE_RESULT
|
assert msg["type"] == "event"
|
||||||
assert not msg["success"]
|
event = msg["event"]
|
||||||
assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR
|
assert event["error"] == "'None' has no attribute 'state'"
|
||||||
|
|
||||||
|
msg = await websocket_client.receive_json()
|
||||||
|
assert msg["id"] == 5
|
||||||
|
assert msg["type"] == "event"
|
||||||
|
event = msg["event"]
|
||||||
|
assert event == {"error": "UndefinedError: 'explode' is undefined"}
|
||||||
|
|
||||||
|
assert "Template variable error" not in caplog.text
|
||||||
|
assert "Template variable warning" not in caplog.text
|
||||||
assert "TemplateError" not in caplog.text
|
assert "TemplateError" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_render_template_with_delayed_error_2(
|
||||||
|
hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture
|
||||||
|
) -> None:
|
||||||
|
"""Test a template with an error that only happens after a state change.
|
||||||
|
|
||||||
|
In this test report_errors is disabled.
|
||||||
|
"""
|
||||||
|
hass.states.async_set("sensor.test", "on")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
template_str = """
|
||||||
|
{% if states.sensor.test.state %}
|
||||||
|
on
|
||||||
|
{% else %}
|
||||||
|
{{ explode + 1 }}
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
await websocket_client.send_json(
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "render_template",
|
||||||
|
"template": template_str,
|
||||||
|
"report_errors": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
msg = await websocket_client.receive_json()
|
||||||
|
assert msg["id"] == 5
|
||||||
|
assert msg["type"] == const.TYPE_RESULT
|
||||||
|
assert msg["success"]
|
||||||
|
|
||||||
|
hass.states.async_remove("sensor.test")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
msg = await websocket_client.receive_json()
|
||||||
|
assert msg["id"] == 5
|
||||||
|
assert msg["type"] == "event"
|
||||||
|
event = msg["event"]
|
||||||
|
assert event == {
|
||||||
|
"result": "on",
|
||||||
|
"listeners": {
|
||||||
|
"all": False,
|
||||||
|
"domains": [],
|
||||||
|
"entities": ["sensor.test"],
|
||||||
|
"time": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert "Template variable warning" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
async def test_render_template_with_timeout(
|
async def test_render_template_with_timeout(
|
||||||
hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture
|
hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
@ -4466,15 +4466,25 @@ async def test_parse_result(hass: HomeAssistant) -> None:
|
||||||
assert template.Template(tpl, hass).async_render() == result
|
assert template.Template(tpl, hass).async_render() == result
|
||||||
|
|
||||||
|
|
||||||
async def test_undefined_variable(
|
@pytest.mark.parametrize(
|
||||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
"template_string",
|
||||||
|
[
|
||||||
|
"{{ no_such_variable }}",
|
||||||
|
"{{ no_such_variable and True }}",
|
||||||
|
"{{ no_such_variable | join(', ') }}",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_undefined_symbol_warnings(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
template_string: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test a warning is logged on undefined variables."""
|
"""Test a warning is logged on undefined variables."""
|
||||||
tpl = template.Template("{{ no_such_variable }}", hass)
|
tpl = template.Template(template_string, hass)
|
||||||
assert tpl.async_render() == ""
|
assert tpl.async_render() == ""
|
||||||
assert (
|
assert (
|
||||||
"Template variable warning: 'no_such_variable' is undefined when rendering "
|
"Template variable warning: 'no_such_variable' is undefined when rendering "
|
||||||
"'{{ no_such_variable }}'" in caplog.text
|
f"'{template_string}'" in caplog.text
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue