Store original result on template results (#42391)
* Store original result on template results * Fix shell command testpull/42516/head
parent
9c6351c1b3
commit
1fb18580b2
|
@ -486,7 +486,11 @@ def string(value: Any) -> str:
|
||||||
"""Coerce value to string, except for None."""
|
"""Coerce value to string, except for None."""
|
||||||
if value is None:
|
if value is None:
|
||||||
raise vol.Invalid("string value is None")
|
raise vol.Invalid("string value is None")
|
||||||
if isinstance(value, (list, dict)):
|
|
||||||
|
if isinstance(value, template_helper.ResultWrapper):
|
||||||
|
value = value.render_result
|
||||||
|
|
||||||
|
elif isinstance(value, (list, dict)):
|
||||||
raise vol.Invalid("value should be a string")
|
raise vol.Invalid("value should be a string")
|
||||||
|
|
||||||
return str(value)
|
return str(value)
|
||||||
|
|
|
@ -11,7 +11,7 @@ import math
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
from typing import Any, Generator, Iterable, List, Optional, Union
|
from typing import Any, Dict, Generator, Iterable, List, Optional, Type, Union
|
||||||
from urllib.parse import urlencode as urllib_urlencode
|
from urllib.parse import urlencode as urllib_urlencode
|
||||||
import weakref
|
import weakref
|
||||||
|
|
||||||
|
@ -124,6 +124,43 @@ def is_template_string(maybe_template: str) -> bool:
|
||||||
return _RE_JINJA_DELIMITERS.search(maybe_template) is not None
|
return _RE_JINJA_DELIMITERS.search(maybe_template) is not None
|
||||||
|
|
||||||
|
|
||||||
|
class ResultWrapper:
|
||||||
|
"""Result wrapper class to store render result."""
|
||||||
|
|
||||||
|
render_result: str
|
||||||
|
|
||||||
|
|
||||||
|
def gen_result_wrapper(kls):
|
||||||
|
"""Generate a result wrapper."""
|
||||||
|
|
||||||
|
class Wrapper(kls, ResultWrapper):
|
||||||
|
"""Wrapper of a kls that can store render_result."""
|
||||||
|
|
||||||
|
def __init__(self, value: kls, render_result: str) -> None:
|
||||||
|
super().__init__(value)
|
||||||
|
self.render_result = render_result
|
||||||
|
|
||||||
|
return Wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class TupleWrapper(tuple, ResultWrapper):
|
||||||
|
"""Wrap a tuple."""
|
||||||
|
|
||||||
|
def __new__(cls, value: tuple, render_result: str) -> "TupleWrapper":
|
||||||
|
"""Create a new tuple class."""
|
||||||
|
return super().__new__(cls, tuple(value))
|
||||||
|
|
||||||
|
def __init__(self, value: tuple, render_result: str):
|
||||||
|
"""Initialize a new tuple class."""
|
||||||
|
self.render_result = render_result
|
||||||
|
|
||||||
|
|
||||||
|
RESULT_WRAPPERS: Dict[Type, Type] = {
|
||||||
|
kls: gen_result_wrapper(kls) for kls in (list, dict, set)
|
||||||
|
}
|
||||||
|
RESULT_WRAPPERS[tuple] = TupleWrapper
|
||||||
|
|
||||||
|
|
||||||
def extract_entities(
|
def extract_entities(
|
||||||
hass: HomeAssistantType,
|
hass: HomeAssistantType,
|
||||||
template: Optional[str],
|
template: Optional[str],
|
||||||
|
@ -285,7 +322,7 @@ class Template:
|
||||||
if not isinstance(template, str):
|
if not isinstance(template, str):
|
||||||
raise TypeError("Expected template to be a string")
|
raise TypeError("Expected template to be a string")
|
||||||
|
|
||||||
self.template: str = template
|
self.template: str = template.strip()
|
||||||
self._compiled_code = None
|
self._compiled_code = None
|
||||||
self._compiled = None
|
self._compiled = None
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
|
@ -322,7 +359,9 @@ class Template:
|
||||||
def render(self, variables: TemplateVarsType = None, **kwargs: Any) -> Any:
|
def render(self, variables: TemplateVarsType = None, **kwargs: Any) -> Any:
|
||||||
"""Render given template."""
|
"""Render given template."""
|
||||||
if self.is_static:
|
if self.is_static:
|
||||||
return self.template.strip()
|
if self.hass.config.legacy_templates:
|
||||||
|
return self.template
|
||||||
|
return self._parse_result(self.template)
|
||||||
|
|
||||||
if variables is not None:
|
if variables is not None:
|
||||||
kwargs.update(variables)
|
kwargs.update(variables)
|
||||||
|
@ -338,7 +377,9 @@ class Template:
|
||||||
This method must be run in the event loop.
|
This method must be run in the event loop.
|
||||||
"""
|
"""
|
||||||
if self.is_static:
|
if self.is_static:
|
||||||
return self.template.strip()
|
if self.hass.config.legacy_templates:
|
||||||
|
return self.template
|
||||||
|
return self._parse_result(self.template)
|
||||||
|
|
||||||
compiled = self._compiled or self._ensure_compiled()
|
compiled = self._compiled or self._ensure_compiled()
|
||||||
|
|
||||||
|
@ -352,18 +393,27 @@ class Template:
|
||||||
|
|
||||||
render_result = render_result.strip()
|
render_result = render_result.strip()
|
||||||
|
|
||||||
if not self.hass.config.legacy_templates:
|
if self.hass.config.legacy_templates:
|
||||||
try:
|
return render_result
|
||||||
result = literal_eval(render_result)
|
|
||||||
|
|
||||||
# If the literal_eval result is a string, use the original
|
return self._parse_result(render_result)
|
||||||
# render, by not returning right here. The evaluation of strings
|
|
||||||
# resulting in strings impacts quotes, to avoid unexpected
|
def _parse_result(self, render_result: str) -> Any:
|
||||||
# output; use the original render instead of the evaluated one.
|
"""Parse the result."""
|
||||||
if not isinstance(result, str):
|
try:
|
||||||
return result
|
result = literal_eval(render_result)
|
||||||
except (ValueError, SyntaxError, MemoryError):
|
|
||||||
pass
|
if type(result) in RESULT_WRAPPERS:
|
||||||
|
result = RESULT_WRAPPERS[type(result)](result, render_result)
|
||||||
|
|
||||||
|
# If the literal_eval result is a string, use the original
|
||||||
|
# render, by not returning right here. The evaluation of strings
|
||||||
|
# resulting in strings impacts quotes, to avoid unexpected
|
||||||
|
# output; use the original render instead of the evaluated one.
|
||||||
|
if not isinstance(result, str):
|
||||||
|
return result
|
||||||
|
except (ValueError, SyntaxError, MemoryError):
|
||||||
|
pass
|
||||||
|
|
||||||
return render_result
|
return render_result
|
||||||
|
|
||||||
|
|
|
@ -174,7 +174,7 @@ async def test_do_no_run_forever(hass, caplog):
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
shell_command.DOMAIN,
|
shell_command.DOMAIN,
|
||||||
{shell_command.DOMAIN: {"test_service": "sleep 10000"}},
|
{shell_command.DOMAIN: {"test_service": "sleep 10000s"}},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import pytest
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant
|
import homeassistant
|
||||||
import homeassistant.helpers.config_validation as cv
|
from homeassistant.helpers import config_validation as cv, template
|
||||||
|
|
||||||
from tests.async_mock import Mock, patch
|
from tests.async_mock import Mock, patch
|
||||||
|
|
||||||
|
@ -365,7 +365,7 @@ def test_slug():
|
||||||
schema(value)
|
schema(value)
|
||||||
|
|
||||||
|
|
||||||
def test_string():
|
def test_string(hass):
|
||||||
"""Test string validation."""
|
"""Test string validation."""
|
||||||
schema = vol.Schema(cv.string)
|
schema = vol.Schema(cv.string)
|
||||||
|
|
||||||
|
@ -381,6 +381,19 @@ def test_string():
|
||||||
for value in (True, 1, "hello"):
|
for value in (True, 1, "hello"):
|
||||||
schema(value)
|
schema(value)
|
||||||
|
|
||||||
|
# Test template support
|
||||||
|
for text, native in (
|
||||||
|
("[1, 2]", [1, 2]),
|
||||||
|
("{1, 2}", {1, 2}),
|
||||||
|
("(1, 2)", (1, 2)),
|
||||||
|
('{"hello": True}', {"hello": True}),
|
||||||
|
):
|
||||||
|
tpl = template.Template(text, hass)
|
||||||
|
result = tpl.async_render()
|
||||||
|
assert isinstance(result, template.ResultWrapper)
|
||||||
|
assert result == native
|
||||||
|
assert schema(result) == text
|
||||||
|
|
||||||
|
|
||||||
def test_string_with_no_html():
|
def test_string_with_no_html():
|
||||||
"""Test string with no html validation."""
|
"""Test string with no html validation."""
|
||||||
|
|
|
@ -2651,3 +2651,25 @@ async def test_legacy_templates(hass):
|
||||||
template.Template("{{ states.sensor.temperature.state }}", hass).async_render()
|
template.Template("{{ states.sensor.temperature.state }}", hass).async_render()
|
||||||
== "12"
|
== "12"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_is_static_still_ast_evals(hass):
|
||||||
|
"""Test is_static still convers to native type."""
|
||||||
|
tpl = template.Template("[1, 2]", hass)
|
||||||
|
assert tpl.is_static
|
||||||
|
assert tpl.async_render() == [1, 2]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_result_wrappers(hass):
|
||||||
|
"""Test result wrappers."""
|
||||||
|
for text, native in (
|
||||||
|
("[1, 2]", [1, 2]),
|
||||||
|
("{1, 2}", {1, 2}),
|
||||||
|
("(1, 2)", (1, 2)),
|
||||||
|
('{"hello": True}', {"hello": True}),
|
||||||
|
):
|
||||||
|
tpl = template.Template(text, hass)
|
||||||
|
result = tpl.async_render()
|
||||||
|
assert isinstance(result, template.ResultWrapper)
|
||||||
|
assert result == native
|
||||||
|
assert result.render_result == text
|
||||||
|
|
Loading…
Reference in New Issue