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

This commit is contained in:
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("entity_ids"): cv.entity_ids,
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,
} }
) )
@decorators.async_response @decorators.async_response
@ -303,7 +304,9 @@ async def handle_render_template(hass, connection, msg):
if timeout: if timeout:
try: 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: 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))
return return
@ -337,6 +340,7 @@ async def handle_render_template(hass, connection, msg):
[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"],
) )
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))

View File

@ -790,12 +790,14 @@ class _TrackTemplateResultInfo:
self._track_state_changes: _TrackStateChangeFiltered | None = None self._track_state_changes: _TrackStateChangeFiltered | None = None
self._time_listeners: dict[Template, Callable] = {} 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.""" """Activation of template tracking."""
for track_template_ in self._track_templates: for track_template_ in self._track_templates:
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(variables) self._info[template] = info = template.async_render_to_info(
variables, strict=strict
)
if info.exception: if info.exception:
if raise_on_template_error: if raise_on_template_error:
@ -1022,6 +1024,7 @@ def async_track_template_result(
track_templates: Iterable[TrackTemplate], track_templates: Iterable[TrackTemplate],
action: TrackTemplateResultListener, action: TrackTemplateResultListener,
raise_on_template_error: bool = False, raise_on_template_error: bool = False,
strict: 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.
@ -1050,6 +1053,8 @@ def async_track_template_result(
processing the template during setup, the system processing the template during setup, the system
will raise the exception instead of setting up will raise the exception instead of setting up
tracking. tracking.
strict
When set to True, raise on undefined variables.
Returns Returns
------- -------
@ -1057,7 +1062,7 @@ def async_track_template_result(
""" """
tracker = _TrackTemplateResultInfo(hass, track_templates, action) tracker = _TrackTemplateResultInfo(hass, track_templates, action)
tracker.async_setup(raise_on_template_error) tracker.async_setup(raise_on_template_error, strict=strict)
return tracker return tracker

View File

@ -15,6 +15,7 @@ import math
from operator import attrgetter from operator import attrgetter
import random import random
import re import re
import sys
from typing import Any, Generator, Iterable, cast from typing import Any, Generator, Iterable, cast
from urllib.parse import urlencode as urllib_urlencode from urllib.parse import urlencode as urllib_urlencode
import weakref import weakref
@ -57,6 +58,7 @@ DATE_STR_FORMAT = "%Y-%m-%d %H:%M:%S"
_RENDER_INFO = "template.render_info" _RENDER_INFO = "template.render_info"
_ENVIRONMENT = "template.environment" _ENVIRONMENT = "template.environment"
_ENVIRONMENT_LIMITED = "template.environment_limited" _ENVIRONMENT_LIMITED = "template.environment_limited"
_ENVIRONMENT_STRICT = "template.environment_strict"
_RE_JINJA_DELIMITERS = re.compile(r"\{%|\{\{|\{#") _RE_JINJA_DELIMITERS = re.compile(r"\{%|\{\{|\{#")
# Match "simple" ints and floats. -1.0, 1, +5, 5.0 # Match "simple" ints and floats. -1.0, 1, +5, 5.0
@ -292,7 +294,9 @@ class Template:
"is_static", "is_static",
"_compiled_code", "_compiled_code",
"_compiled", "_compiled",
"_exc_info",
"_limited", "_limited",
"_strict",
) )
def __init__(self, template, hass=None): def __init__(self, template, hass=None):
@ -305,16 +309,23 @@ class Template:
self._compiled: jinja2.Template | None = None self._compiled: jinja2.Template | None = None
self.hass = hass self.hass = hass
self.is_static = not is_template_string(template) self.is_static = not is_template_string(template)
self._exc_info = None
self._limited = None self._limited = None
self._strict = None
@property @property
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
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) ret: TemplateEnvironment | None = self.hass.data.get(wanted_env)
if ret is None: 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 return ret
def ensure_valid(self) -> None: def ensure_valid(self) -> None:
@ -354,6 +365,7 @@ class Template:
variables: TemplateVarsType = None, variables: TemplateVarsType = None,
parse_result: bool = True, parse_result: bool = True,
limited: bool = False, limited: bool = False,
strict: bool = False,
**kwargs: Any, **kwargs: Any,
) -> Any: ) -> Any:
"""Render given template. """Render given template.
@ -367,7 +379,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) compiled = self._compiled or self._ensure_compiled(limited, strict)
if variables is not None: if variables is not None:
kwargs.update(variables) kwargs.update(variables)
@ -418,7 +430,11 @@ class Template:
return render_result return render_result
async def async_render_will_timeout( 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: ) -> bool:
"""Check to see if rendering a template will timeout during render. """Check to see if rendering a template will timeout during render.
@ -436,11 +452,12 @@ class Template:
if self.is_static: if self.is_static:
return False return False
compiled = self._compiled or self._ensure_compiled() compiled = self._compiled or self._ensure_compiled(strict=strict)
if variables is not None: if variables is not None:
kwargs.update(variables) kwargs.update(variables)
self._exc_info = None
finish_event = asyncio.Event() finish_event = asyncio.Event()
def _render_template() -> None: def _render_template() -> None:
@ -448,6 +465,8 @@ class Template:
_render_with_context(self.template, compiled, **kwargs) _render_with_context(self.template, compiled, **kwargs)
except TimeoutError: except TimeoutError:
pass pass
except Exception: # pylint: disable=broad-except
self._exc_info = sys.exc_info()
finally: finally:
run_callback_threadsafe(self.hass.loop, finish_event.set) 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 = ThreadWithException(target=_render_template)
template_render_thread.start() template_render_thread.start()
await asyncio.wait_for(finish_event.wait(), timeout=timeout) 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: except asyncio.TimeoutError:
template_render_thread.raise_exc(TimeoutError) template_render_thread.raise_exc(TimeoutError)
return True return True
@ -465,7 +486,7 @@ class Template:
@callback @callback
def async_render_to_info( def async_render_to_info(
self, variables: TemplateVarsType = None, **kwargs: Any self, variables: TemplateVarsType = None, strict: bool = False, **kwargs: Any
) -> RenderInfo: ) -> RenderInfo:
"""Render the template and collect an entity filter.""" """Render the template and collect an entity filter."""
assert self.hass and _RENDER_INFO not in self.hass.data assert self.hass and _RENDER_INFO not in self.hass.data
@ -480,7 +501,7 @@ class Template:
self.hass.data[_RENDER_INFO] = render_info self.hass.data[_RENDER_INFO] = render_info
try: try:
render_info._result = self.async_render(variables, **kwargs) render_info._result = self.async_render(variables, strict=strict, **kwargs)
except TemplateError as ex: except TemplateError as ex:
render_info.exception = ex render_info.exception = ex
finally: finally:
@ -540,7 +561,9 @@ 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(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.""" """Bind a template to a specific hass instance."""
self.ensure_valid() self.ensure_valid()
@ -548,8 +571,13 @@ class Template:
assert ( assert (
self._limited is None or self._limited == limited self._limited is None or self._limited == limited
), "can't change between limited and non limited template" ), "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._limited = limited
self._strict = strict
env = self._env env = self._env
self._compiled = cast( self._compiled = cast(
@ -1369,9 +1397,13 @@ class LoggingUndefined(jinja2.Undefined):
class TemplateEnvironment(ImmutableSandboxedEnvironment): class TemplateEnvironment(ImmutableSandboxedEnvironment):
"""The Home Assistant template environment.""" """The Home Assistant template environment."""
def __init__(self, hass, limited=False): def __init__(self, hass, limited=False, strict=False):
"""Initialise template environment.""" """Initialise template environment."""
super().__init__(undefined=LoggingUndefined) 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()
self.filters["round"] = forgiving_round 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.""" """Test a template with an error."""
await websocket_client.send_json( 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() 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 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 "TemplateError" 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.""" """Test a template with an error with a timeout."""
await websocket_client.send_json( await websocket_client.send_json(
{ {
"id": 5, "id": 5,
"type": "render_template", "type": "render_template",
"template": "{{ now() | rando }}", "template": template,
"timeout": 5, "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 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 "TemplateError" 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) tmp = template.Template("{{ states | count }}", hass)
assert await tmp.async_render_will_timeout(3) is False 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) tmp3 = template.Template("static", hass)
assert await tmp3.async_render_will_timeout(3) is False 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 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): async def test_lights(hass):
"""Test we can sort lights.""" """Test we can sort lights."""