Store original result on template results (#42391)

* Store original result on template results

* Fix shell command test
pull/42516/head
Paulus Schoutsen 2020-10-26 11:30:58 +01:00
parent 9c6351c1b3
commit 1fb18580b2
5 changed files with 108 additions and 19 deletions

View File

@ -486,7 +486,11 @@ def string(value: Any) -> str:
"""Coerce value to string, except for None."""
if 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")
return str(value)

View File

@ -11,7 +11,7 @@ import math
from operator import attrgetter
import random
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
import weakref
@ -124,6 +124,43 @@ def is_template_string(maybe_template: str) -> bool:
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(
hass: HomeAssistantType,
template: Optional[str],
@ -285,7 +322,7 @@ class Template:
if not isinstance(template, str):
raise TypeError("Expected template to be a string")
self.template: str = template
self.template: str = template.strip()
self._compiled_code = None
self._compiled = None
self.hass = hass
@ -322,7 +359,9 @@ class Template:
def render(self, variables: TemplateVarsType = None, **kwargs: Any) -> Any:
"""Render given template."""
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:
kwargs.update(variables)
@ -338,7 +377,9 @@ class Template:
This method must be run in the event loop.
"""
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()
@ -352,18 +393,27 @@ class Template:
render_result = render_result.strip()
if not self.hass.config.legacy_templates:
try:
result = literal_eval(render_result)
if self.hass.config.legacy_templates:
return 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 self._parse_result(render_result)
def _parse_result(self, render_result: str) -> Any:
"""Parse the result."""
try:
result = literal_eval(render_result)
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

View File

@ -174,7 +174,7 @@ async def test_do_no_run_forever(hass, caplog):
assert await async_setup_component(
hass,
shell_command.DOMAIN,
{shell_command.DOMAIN: {"test_service": "sleep 10000"}},
{shell_command.DOMAIN: {"test_service": "sleep 10000s"}},
)
await hass.async_block_till_done()

View File

@ -9,7 +9,7 @@ import pytest
import voluptuous as vol
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
@ -365,7 +365,7 @@ def test_slug():
schema(value)
def test_string():
def test_string(hass):
"""Test string validation."""
schema = vol.Schema(cv.string)
@ -381,6 +381,19 @@ def test_string():
for value in (True, 1, "hello"):
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():
"""Test string with no html validation."""

View File

@ -2651,3 +2651,25 @@ async def test_legacy_templates(hass):
template.Template("{{ states.sensor.temperature.state }}", hass).async_render()
== "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