Improve warnings on undefined template errors (#48713)
parent
5f2a666e76
commit
0df9a8ec38
|
@ -6,6 +6,7 @@ import asyncio
|
||||||
import base64
|
import base64
|
||||||
import collections.abc
|
import collections.abc
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
from contextvars import ContextVar
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from functools import partial, wraps
|
from functools import partial, wraps
|
||||||
import json
|
import json
|
||||||
|
@ -79,6 +80,8 @@ _COLLECTABLE_STATE_ATTRIBUTES = {
|
||||||
ALL_STATES_RATE_LIMIT = timedelta(minutes=1)
|
ALL_STATES_RATE_LIMIT = timedelta(minutes=1)
|
||||||
DOMAIN_STATES_RATE_LIMIT = timedelta(seconds=1)
|
DOMAIN_STATES_RATE_LIMIT = timedelta(seconds=1)
|
||||||
|
|
||||||
|
template_cv: ContextVar[str | None] = ContextVar("template_cv", default=None)
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
@bind_hass
|
||||||
def attach(hass: HomeAssistant, obj: Any) -> None:
|
def attach(hass: HomeAssistant, obj: Any) -> None:
|
||||||
|
@ -299,7 +302,7 @@ class Template:
|
||||||
|
|
||||||
self.template: str = template.strip()
|
self.template: str = template.strip()
|
||||||
self._compiled_code = None
|
self._compiled_code = None
|
||||||
self._compiled: 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._limited = None
|
self._limited = None
|
||||||
|
@ -370,7 +373,7 @@ class Template:
|
||||||
kwargs.update(variables)
|
kwargs.update(variables)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
render_result = compiled.render(kwargs)
|
render_result = _render_with_context(self.template, compiled, **kwargs)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
raise TemplateError(err) from err
|
raise TemplateError(err) from err
|
||||||
|
|
||||||
|
@ -442,7 +445,7 @@ class Template:
|
||||||
|
|
||||||
def _render_template() -> None:
|
def _render_template() -> None:
|
||||||
try:
|
try:
|
||||||
compiled.render(kwargs)
|
_render_with_context(self.template, compiled, **kwargs)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
|
@ -524,7 +527,9 @@ class Template:
|
||||||
variables["value_json"] = json.loads(value)
|
variables["value_json"] = json.loads(value)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self._compiled.render(variables).strip()
|
return _render_with_context(
|
||||||
|
self.template, self._compiled, **variables
|
||||||
|
).strip()
|
||||||
except jinja2.TemplateError as ex:
|
except jinja2.TemplateError as ex:
|
||||||
if error_value is _SENTINEL:
|
if error_value is _SENTINEL:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
|
@ -535,7 +540,7 @@ 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) -> Template:
|
def _ensure_compiled(self, limited: 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,7 +553,7 @@ class Template:
|
||||||
env = self._env
|
env = self._env
|
||||||
|
|
||||||
self._compiled = cast(
|
self._compiled = cast(
|
||||||
Template,
|
jinja2.Template,
|
||||||
jinja2.Template.from_code(env, self._compiled_code, env.globals, None),
|
jinja2.Template.from_code(env, self._compiled_code, env.globals, None),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1314,12 +1319,59 @@ def urlencode(value):
|
||||||
return urllib_urlencode(value).encode("utf-8")
|
return urllib_urlencode(value).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _render_with_context(
|
||||||
|
template_str: str, template: jinja2.Template, **kwargs: Any
|
||||||
|
) -> str:
|
||||||
|
"""Store template being rendered in a ContextVar to aid error handling."""
|
||||||
|
template_cv.set(template_str)
|
||||||
|
return template.render(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingUndefined(jinja2.Undefined):
|
||||||
|
"""Log on undefined variables."""
|
||||||
|
|
||||||
|
def _log_message(self):
|
||||||
|
template = template_cv.get() or ""
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Template variable warning: %s when rendering '%s'",
|
||||||
|
self._undefined_message,
|
||||||
|
template,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _fail_with_undefined_error(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
return super()._fail_with_undefined_error(*args, **kwargs)
|
||||||
|
except self._undefined_exception as ex:
|
||||||
|
template = template_cv.get() or ""
|
||||||
|
_LOGGER.error(
|
||||||
|
"Template variable error: %s when rendering '%s'",
|
||||||
|
self._undefined_message,
|
||||||
|
template,
|
||||||
|
)
|
||||||
|
raise ex
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Log undefined __str___."""
|
||||||
|
self._log_message()
|
||||||
|
return super().__str__()
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""Log undefined __iter___."""
|
||||||
|
self._log_message()
|
||||||
|
return super().__iter__()
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
"""Log undefined __bool___."""
|
||||||
|
self._log_message()
|
||||||
|
return super().__bool__()
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
"""Initialise template environment."""
|
"""Initialise template environment."""
|
||||||
super().__init__(undefined=jinja2.make_logging_undefined(logger=_LOGGER))
|
super().__init__(undefined=LoggingUndefined)
|
||||||
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
|
||||||
|
|
|
@ -2503,4 +2503,7 @@ async def test_undefined_variable(hass, caplog):
|
||||||
"""Test a warning is logged on undefined variables."""
|
"""Test a warning is logged on undefined variables."""
|
||||||
tpl = template.Template("{{ no_such_variable }}", hass)
|
tpl = template.Template("{{ no_such_variable }}", hass)
|
||||||
assert tpl.async_render() == ""
|
assert tpl.async_render() == ""
|
||||||
assert "Template variable warning: no_such_variable is undefined" in caplog.text
|
assert (
|
||||||
|
"Template variable warning: 'no_such_variable' is undefined when rendering '{{ no_such_variable }}'"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue