Ensure all jinja2 errors are trapped and displayed in the developer tools (#40624)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
J. Nick Koston 2020-09-26 17:03:32 -05:00 committed by GitHub
parent 3261a904da
commit 57b7559832
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 89 additions and 22 deletions

View file

@ -17,6 +17,7 @@ from homeassistant.exceptions import (
from homeassistant.helpers import config_validation as cv, entity from homeassistant.helpers import config_validation as cv, entity
from homeassistant.helpers.event import TrackTemplate, async_track_template_result from homeassistant.helpers.event import TrackTemplate, async_track_template_result
from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers.template import Template
from homeassistant.loader import IntegrationNotFound, async_get_integration from homeassistant.loader import IntegrationNotFound, async_get_integration
from . import const, decorators, messages from . import const, decorators, messages
@ -242,16 +243,15 @@ def handle_ping(hass, connection, msg):
@decorators.websocket_command( @decorators.websocket_command(
{ {
vol.Required("type"): "render_template", vol.Required("type"): "render_template",
vol.Required("template"): cv.template, vol.Required("template"): str,
vol.Optional("entity_ids"): cv.entity_ids, vol.Optional("entity_ids"): cv.entity_ids,
vol.Optional("variables"): dict, vol.Optional("variables"): dict,
} }
) )
def handle_render_template(hass, connection, msg): def handle_render_template(hass, connection, msg):
"""Handle render_template command.""" """Handle render_template command."""
template = msg["template"] template_str = msg["template"]
template.hass = hass template = Template(template_str, hass)
variables = msg.get("variables") variables = msg.get("variables")
info = None info = None
@ -261,13 +261,8 @@ def handle_render_template(hass, connection, msg):
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):
_LOGGER.error( connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(result))
"TemplateError('%s') " "while processing template '%s'", return
result,
track_template_result.template,
)
result = None
connection.send_message( connection.send_message(
messages.event_message( messages.event_message(
@ -275,9 +270,16 @@ def handle_render_template(hass, connection, msg):
) )
) )
try:
info = async_track_template_result( info = async_track_template_result(
hass, [TrackTemplate(template, variables)], _template_listener hass,
[TrackTemplate(template, variables)],
_template_listener,
raise_on_template_error=True,
) )
except TemplateError as ex:
connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex))
return
connection.subscriptions[msg["id"]] = info.async_remove connection.subscriptions[msg["id"]] = info.async_remove

View file

@ -29,6 +29,7 @@ ERR_UNKNOWN_COMMAND = "unknown_command"
ERR_UNKNOWN_ERROR = "unknown_error" ERR_UNKNOWN_ERROR = "unknown_error"
ERR_UNAUTHORIZED = "unauthorized" ERR_UNAUTHORIZED = "unauthorized"
ERR_TIMEOUT = "timeout" ERR_TIMEOUT = "timeout"
ERR_TEMPLATE_ERROR = "template_error"
TYPE_RESULT = "result" TYPE_RESULT = "result"

View file

@ -565,7 +565,7 @@ class _TrackTemplateResultInfo:
self._last_domains: Set = set() self._last_domains: Set = set()
self._last_entities: Set = set() self._last_entities: Set = set()
def async_setup(self) -> None: def async_setup(self, raise_on_template_error: bool) -> 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
@ -573,6 +573,8 @@ class _TrackTemplateResultInfo:
self._info[template] = template.async_render_to_info(variables) self._info[template] = template.async_render_to_info(variables)
if self._info[template].exception: if self._info[template].exception:
if raise_on_template_error:
raise self._info[template].exception
_LOGGER.error( _LOGGER.error(
"Error while processing template: %s", "Error while processing template: %s",
track_template_.template, track_template_.template,
@ -812,6 +814,7 @@ def async_track_template_result(
hass: HomeAssistant, hass: HomeAssistant,
track_templates: Iterable[TrackTemplate], track_templates: Iterable[TrackTemplate],
action: TrackTemplateResultListener, action: TrackTemplateResultListener,
raise_on_template_error: bool = False,
) -> _TrackTemplateResultInfo: ) -> _TrackTemplateResultInfo:
"""Add a listener that fires when a the result of a template changes. """Add a listener that fires when a the result of a template changes.
@ -833,9 +836,13 @@ def async_track_template_result(
Home assistant object. Home assistant object.
track_templates track_templates
An iterable of TrackTemplate. An iterable of TrackTemplate.
action action
Callable to call with results. Callable to call with results.
raise_on_template_error
When set to True, if there is an exception
processing the template during setup, the system
will raise the exception instead of setting up
tracking.
Returns Returns
------- -------
@ -843,7 +850,7 @@ def async_track_template_result(
""" """
tracker = _TrackTemplateResultInfo(hass, track_templates, action) tracker = _TrackTemplateResultInfo(hass, track_templates, action)
tracker.async_setup() tracker.async_setup(raise_on_template_error)
return tracker return tracker

View file

@ -266,7 +266,7 @@ class Template:
try: try:
self._compiled_code = self._env.compile(self.template) self._compiled_code = self._env.compile(self.template)
except jinja2.exceptions.TemplateSyntaxError as err: except jinja2.TemplateError as err:
raise TemplateError(err) from err raise TemplateError(err) from err
def extract_entities( def extract_entities(

View file

@ -484,21 +484,59 @@ async def test_render_template_with_error(
) )
msg = await websocket_client.receive_json() msg = await websocket_client.receive_json()
assert msg["id"] == 5
assert msg["type"] == const.TYPE_RESULT
assert not msg["success"]
assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR
assert "TemplateError" not in caplog.text
async def test_render_template_with_delayed_error(
hass, websocket_client, hass_admin_user, caplog
):
"""Test a template with an error that only happens after a state change."""
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}
)
await hass.async_block_till_done()
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"]
hass.states.async_remove("sensor.test")
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"] == "event" assert msg["type"] == "event"
event = msg["event"] event = msg["event"]
assert event == { assert event == {
"result": None, "result": "on",
"listeners": {"all": True, "domains": [], "entities": []}, "listeners": {"all": False, "domains": [], "entities": ["sensor.test"]},
} }
assert "my_unknown_var" in caplog.text msg = await websocket_client.receive_json()
assert "TemplateError" in caplog.text assert msg["id"] == 5
assert msg["type"] == const.TYPE_RESULT
assert not msg["success"]
assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR
assert "TemplateError" not in caplog.text
async def test_render_template_returns_with_match_all( async def test_render_template_returns_with_match_all(

View file

@ -1460,6 +1460,25 @@ async def test_async_track_template_result_multiple_templates_mixing_domain(hass
] ]
async def test_async_track_template_result_raise_on_template_error(hass):
"""Test that we raise as soon as we encounter a failed template."""
with pytest.raises(TemplateError):
async_track_template_result(
hass,
[
TrackTemplate(
Template(
"{{ states.switch | function_that_does_not_exist | list }}"
),
None,
),
],
ha.callback(lambda event, updates: None),
raise_on_template_error=True,
)
async def test_track_same_state_simple_no_trigger(hass): async def test_track_same_state_simple_no_trigger(hass):
"""Test track_same_change with no trigger.""" """Test track_same_change with no trigger."""
callback_runs = [] callback_runs = []