Don't log template errors from developer tool (#48933)

pull/48961/head
Erik Montnemery 2021-04-09 21:10:02 +02:00 committed by GitHub
parent 43335953a2
commit 16196e2e16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 89 additions and 21 deletions

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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."""