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.""" """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)

View File

@ -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

View File

@ -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()

View File

@ -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."""

View File

@ -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