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:
parent
3261a904da
commit
57b7559832
6 changed files with 89 additions and 22 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue