Don't log template errors from developer tool (#48933)
parent
43335953a2
commit
16196e2e16
|
@ -290,6 +290,7 @@ def handle_ping(hass, connection, msg):
|
|||
vol.Optional("entity_ids"): cv.entity_ids,
|
||||
vol.Optional("variables"): dict,
|
||||
vol.Optional("timeout"): vol.Coerce(float),
|
||||
vol.Optional("strict", default=False): bool,
|
||||
}
|
||||
)
|
||||
@decorators.async_response
|
||||
|
@ -303,7 +304,9 @@ async def handle_render_template(hass, connection, msg):
|
|||
|
||||
if timeout:
|
||||
try:
|
||||
timed_out = await template_obj.async_render_will_timeout(timeout)
|
||||
timed_out = await template_obj.async_render_will_timeout(
|
||||
timeout, strict=msg["strict"]
|
||||
)
|
||||
except TemplateError as ex:
|
||||
connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex))
|
||||
return
|
||||
|
@ -337,6 +340,7 @@ async def handle_render_template(hass, connection, msg):
|
|||
[TrackTemplate(template_obj, variables)],
|
||||
_template_listener,
|
||||
raise_on_template_error=True,
|
||||
strict=msg["strict"],
|
||||
)
|
||||
except TemplateError as ex:
|
||||
connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex))
|
||||
|
|
|
@ -790,12 +790,14 @@ class _TrackTemplateResultInfo:
|
|||
self._track_state_changes: _TrackStateChangeFiltered | None = None
|
||||
self._time_listeners: dict[Template, Callable] = {}
|
||||
|
||||
def async_setup(self, raise_on_template_error: bool) -> None:
|
||||
def async_setup(self, raise_on_template_error: bool, strict: bool = False) -> None:
|
||||
"""Activation of template tracking."""
|
||||
for track_template_ in self._track_templates:
|
||||
template = track_template_.template
|
||||
variables = track_template_.variables
|
||||
self._info[template] = info = template.async_render_to_info(variables)
|
||||
self._info[template] = info = template.async_render_to_info(
|
||||
variables, strict=strict
|
||||
)
|
||||
|
||||
if info.exception:
|
||||
if raise_on_template_error:
|
||||
|
@ -1022,6 +1024,7 @@ def async_track_template_result(
|
|||
track_templates: Iterable[TrackTemplate],
|
||||
action: TrackTemplateResultListener,
|
||||
raise_on_template_error: bool = False,
|
||||
strict: bool = False,
|
||||
) -> _TrackTemplateResultInfo:
|
||||
"""Add a listener that fires when the result of a template changes.
|
||||
|
||||
|
@ -1050,6 +1053,8 @@ def async_track_template_result(
|
|||
processing the template during setup, the system
|
||||
will raise the exception instead of setting up
|
||||
tracking.
|
||||
strict
|
||||
When set to True, raise on undefined variables.
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
@ -1057,7 +1062,7 @@ def async_track_template_result(
|
|||
|
||||
"""
|
||||
tracker = _TrackTemplateResultInfo(hass, track_templates, action)
|
||||
tracker.async_setup(raise_on_template_error)
|
||||
tracker.async_setup(raise_on_template_error, strict=strict)
|
||||
return tracker
|
||||
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import math
|
|||
from operator import attrgetter
|
||||
import random
|
||||
import re
|
||||
import sys
|
||||
from typing import Any, Generator, Iterable, cast
|
||||
from urllib.parse import urlencode as urllib_urlencode
|
||||
import weakref
|
||||
|
@ -57,6 +58,7 @@ DATE_STR_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|||
_RENDER_INFO = "template.render_info"
|
||||
_ENVIRONMENT = "template.environment"
|
||||
_ENVIRONMENT_LIMITED = "template.environment_limited"
|
||||
_ENVIRONMENT_STRICT = "template.environment_strict"
|
||||
|
||||
_RE_JINJA_DELIMITERS = re.compile(r"\{%|\{\{|\{#")
|
||||
# Match "simple" ints and floats. -1.0, 1, +5, 5.0
|
||||
|
@ -292,7 +294,9 @@ class Template:
|
|||
"is_static",
|
||||
"_compiled_code",
|
||||
"_compiled",
|
||||
"_exc_info",
|
||||
"_limited",
|
||||
"_strict",
|
||||
)
|
||||
|
||||
def __init__(self, template, hass=None):
|
||||
|
@ -305,16 +309,23 @@ class Template:
|
|||
self._compiled: jinja2.Template | None = None
|
||||
self.hass = hass
|
||||
self.is_static = not is_template_string(template)
|
||||
self._exc_info = None
|
||||
self._limited = None
|
||||
self._strict = None
|
||||
|
||||
@property
|
||||
def _env(self) -> TemplateEnvironment:
|
||||
if self.hass is None:
|
||||
return _NO_HASS_ENV
|
||||
wanted_env = _ENVIRONMENT_LIMITED if self._limited else _ENVIRONMENT
|
||||
if self._limited:
|
||||
wanted_env = _ENVIRONMENT_LIMITED
|
||||
elif self._strict:
|
||||
wanted_env = _ENVIRONMENT_STRICT
|
||||
else:
|
||||
wanted_env = _ENVIRONMENT
|
||||
ret: TemplateEnvironment | None = self.hass.data.get(wanted_env)
|
||||
if ret is None:
|
||||
ret = self.hass.data[wanted_env] = TemplateEnvironment(self.hass, self._limited) # type: ignore[no-untyped-call]
|
||||
ret = self.hass.data[wanted_env] = TemplateEnvironment(self.hass, self._limited, self._strict) # type: ignore[no-untyped-call]
|
||||
return ret
|
||||
|
||||
def ensure_valid(self) -> None:
|
||||
|
@ -354,6 +365,7 @@ class Template:
|
|||
variables: TemplateVarsType = None,
|
||||
parse_result: bool = True,
|
||||
limited: bool = False,
|
||||
strict: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""Render given template.
|
||||
|
@ -367,7 +379,7 @@ class Template:
|
|||
return self.template
|
||||
return self._parse_result(self.template)
|
||||
|
||||
compiled = self._compiled or self._ensure_compiled(limited)
|
||||
compiled = self._compiled or self._ensure_compiled(limited, strict)
|
||||
|
||||
if variables is not None:
|
||||
kwargs.update(variables)
|
||||
|
@ -418,7 +430,11 @@ class Template:
|
|||
return render_result
|
||||
|
||||
async def async_render_will_timeout(
|
||||
self, timeout: float, variables: TemplateVarsType = None, **kwargs: Any
|
||||
self,
|
||||
timeout: float,
|
||||
variables: TemplateVarsType = None,
|
||||
strict: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> bool:
|
||||
"""Check to see if rendering a template will timeout during render.
|
||||
|
||||
|
@ -436,11 +452,12 @@ class Template:
|
|||
if self.is_static:
|
||||
return False
|
||||
|
||||
compiled = self._compiled or self._ensure_compiled()
|
||||
compiled = self._compiled or self._ensure_compiled(strict=strict)
|
||||
|
||||
if variables is not None:
|
||||
kwargs.update(variables)
|
||||
|
||||
self._exc_info = None
|
||||
finish_event = asyncio.Event()
|
||||
|
||||
def _render_template() -> None:
|
||||
|
@ -448,6 +465,8 @@ class Template:
|
|||
_render_with_context(self.template, compiled, **kwargs)
|
||||
except TimeoutError:
|
||||
pass
|
||||
except Exception: # pylint: disable=broad-except
|
||||
self._exc_info = sys.exc_info()
|
||||
finally:
|
||||
run_callback_threadsafe(self.hass.loop, finish_event.set)
|
||||
|
||||
|
@ -455,6 +474,8 @@ class Template:
|
|||
template_render_thread = ThreadWithException(target=_render_template)
|
||||
template_render_thread.start()
|
||||
await asyncio.wait_for(finish_event.wait(), timeout=timeout)
|
||||
if self._exc_info:
|
||||
raise TemplateError(self._exc_info[1].with_traceback(self._exc_info[2]))
|
||||
except asyncio.TimeoutError:
|
||||
template_render_thread.raise_exc(TimeoutError)
|
||||
return True
|
||||
|
@ -465,7 +486,7 @@ class Template:
|
|||
|
||||
@callback
|
||||
def async_render_to_info(
|
||||
self, variables: TemplateVarsType = None, **kwargs: Any
|
||||
self, variables: TemplateVarsType = None, strict: bool = False, **kwargs: Any
|
||||
) -> RenderInfo:
|
||||
"""Render the template and collect an entity filter."""
|
||||
assert self.hass and _RENDER_INFO not in self.hass.data
|
||||
|
@ -480,7 +501,7 @@ class Template:
|
|||
|
||||
self.hass.data[_RENDER_INFO] = render_info
|
||||
try:
|
||||
render_info._result = self.async_render(variables, **kwargs)
|
||||
render_info._result = self.async_render(variables, strict=strict, **kwargs)
|
||||
except TemplateError as ex:
|
||||
render_info.exception = ex
|
||||
finally:
|
||||
|
@ -540,7 +561,9 @@ class Template:
|
|||
)
|
||||
return value if error_value is _SENTINEL else error_value
|
||||
|
||||
def _ensure_compiled(self, limited: bool = False) -> jinja2.Template:
|
||||
def _ensure_compiled(
|
||||
self, limited: bool = False, strict: bool = False
|
||||
) -> jinja2.Template:
|
||||
"""Bind a template to a specific hass instance."""
|
||||
self.ensure_valid()
|
||||
|
||||
|
@ -548,8 +571,13 @@ class Template:
|
|||
assert (
|
||||
self._limited is None or self._limited == limited
|
||||
), "can't change between limited and non limited template"
|
||||
assert (
|
||||
self._strict is None or self._strict == strict
|
||||
), "can't change between strict and non strict template"
|
||||
assert not (strict and limited), "can't combine strict and limited template"
|
||||
|
||||
self._limited = limited
|
||||
self._strict = strict
|
||||
env = self._env
|
||||
|
||||
self._compiled = cast(
|
||||
|
@ -1369,9 +1397,13 @@ class LoggingUndefined(jinja2.Undefined):
|
|||
class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||
"""The Home Assistant template environment."""
|
||||
|
||||
def __init__(self, hass, limited=False):
|
||||
def __init__(self, hass, limited=False, strict=False):
|
||||
"""Initialise template environment."""
|
||||
super().__init__(undefined=LoggingUndefined)
|
||||
if not strict:
|
||||
undefined = LoggingUndefined
|
||||
else:
|
||||
undefined = jinja2.StrictUndefined
|
||||
super().__init__(undefined=undefined)
|
||||
self.hass = hass
|
||||
self.template_cache = weakref.WeakValueDictionary()
|
||||
self.filters["round"] = forgiving_round
|
||||
|
|
|
@ -697,10 +697,19 @@ async def test_render_template_manual_entity_ids_no_longer_needed(
|
|||
}
|
||||
|
||||
|
||||
async def test_render_template_with_error(hass, websocket_client, caplog):
|
||||
@pytest.mark.parametrize(
|
||||
"template",
|
||||
[
|
||||
"{{ my_unknown_func() + 1 }}",
|
||||
"{{ my_unknown_var }}",
|
||||
"{{ my_unknown_var + 1 }}",
|
||||
"{{ now() | unknown_filter }}",
|
||||
],
|
||||
)
|
||||
async def test_render_template_with_error(hass, websocket_client, caplog, template):
|
||||
"""Test a template with an error."""
|
||||
await websocket_client.send_json(
|
||||
{"id": 5, "type": "render_template", "template": "{{ my_unknown_var() + 1 }}"}
|
||||
{"id": 5, "type": "render_template", "template": template, "strict": True}
|
||||
)
|
||||
|
||||
msg = await websocket_client.receive_json()
|
||||
|
@ -709,17 +718,30 @@ async def test_render_template_with_error(hass, websocket_client, caplog):
|
|||
assert not msg["success"]
|
||||
assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR
|
||||
|
||||
assert "Template variable error" not in caplog.text
|
||||
assert "TemplateError" not in caplog.text
|
||||
|
||||
|
||||
async def test_render_template_with_timeout_and_error(hass, websocket_client, caplog):
|
||||
@pytest.mark.parametrize(
|
||||
"template",
|
||||
[
|
||||
"{{ my_unknown_func() + 1 }}",
|
||||
"{{ my_unknown_var }}",
|
||||
"{{ my_unknown_var + 1 }}",
|
||||
"{{ now() | unknown_filter }}",
|
||||
],
|
||||
)
|
||||
async def test_render_template_with_timeout_and_error(
|
||||
hass, websocket_client, caplog, template
|
||||
):
|
||||
"""Test a template with an error with a timeout."""
|
||||
await websocket_client.send_json(
|
||||
{
|
||||
"id": 5,
|
||||
"type": "render_template",
|
||||
"template": "{{ now() | rando }}",
|
||||
"template": template,
|
||||
"timeout": 5,
|
||||
"strict": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -729,6 +751,7 @@ async def test_render_template_with_timeout_and_error(hass, websocket_client, ca
|
|||
assert not msg["success"]
|
||||
assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR
|
||||
|
||||
assert "Template variable error" not in caplog.text
|
||||
assert "TemplateError" not in caplog.text
|
||||
|
||||
|
||||
|
|
|
@ -2267,9 +2267,6 @@ async def test_template_timeout(hass):
|
|||
tmp = template.Template("{{ states | count }}", hass)
|
||||
assert await tmp.async_render_will_timeout(3) is False
|
||||
|
||||
tmp2 = template.Template("{{ error_invalid + 1 }}", hass)
|
||||
assert await tmp2.async_render_will_timeout(3) is False
|
||||
|
||||
tmp3 = template.Template("static", hass)
|
||||
assert await tmp3.async_render_will_timeout(3) is False
|
||||
|
||||
|
@ -2287,6 +2284,13 @@ async def test_template_timeout(hass):
|
|||
assert await tmp5.async_render_will_timeout(0.000001) is True
|
||||
|
||||
|
||||
async def test_template_timeout_raise(hass):
|
||||
"""Test we can raise from."""
|
||||
tmp2 = template.Template("{{ error_invalid + 1 }}", hass)
|
||||
with pytest.raises(TemplateError):
|
||||
assert await tmp2.async_render_will_timeout(3) is False
|
||||
|
||||
|
||||
async def test_lights(hass):
|
||||
"""Test we can sort lights."""
|
||||
|
||||
|
|
Loading…
Reference in New Issue