1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-24 12:59:34 +00:00

render_with_collect method for template (#23283)

* Make entity_filter be a modifiable builder

* Add render_with_collect method

* Use sync render_with_collect and non-class based test case

* Refactor: Template renders to RenderInfo

* Freeze with exception too

* Finish merging test changes

* Removed unused sync interface

* Final bits of the diff
This commit is contained in:
Penny Wood
2019-05-01 10:54:25 +08:00
committed by GitHub
parent 581b16e9fa
commit 5b9d01139d
3 changed files with 474 additions and 90 deletions

View File

@@ -1,25 +1,19 @@
"""Test Home Assistant template helper methods."""
from datetime import datetime
import random
import math
import random
from datetime import datetime
from unittest.mock import patch
import pytest
import pytz
import homeassistant.util.dt as dt_util
from homeassistant.components import group
from homeassistant.const import (LENGTH_METERS, MASS_GRAMS, MATCH_ALL,
PRESSURE_PA, TEMP_CELSIUS, VOLUME_LITERS)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template
from homeassistant.util.unit_system import UnitSystem
from homeassistant.const import (
LENGTH_METERS,
TEMP_CELSIUS,
MASS_GRAMS,
PRESSURE_PA,
VOLUME_LITERS,
MATCH_ALL,
)
import homeassistant.util.dt as dt_util
def _set_up_units(hass):
@@ -29,32 +23,148 @@ def _set_up_units(hass):
MASS_GRAMS, PRESSURE_PA)
def render_to_info(hass, template_str, variables=None):
"""Create render info from template."""
tmp = template.Template(template_str, hass)
return tmp.async_render_to_info(variables)
def extract_entities(hass, template_str, variables=None):
"""Extract entities from a template."""
info = render_to_info(hass, template_str, variables)
# pylint: disable=protected-access
assert not hasattr(info, '_domains')
return info._entities
def assert_result_info(
info, result, entities=None, domains=None, all_states=False):
"""Check result info."""
assert info.result == result
# pylint: disable=protected-access
assert info._all_states == all_states
assert info.filter_lifecycle('invalid_entity_name.somewhere') == all_states
if entities is not None:
assert info._entities == frozenset(entities)
assert all([info.filter(entity) for entity in entities])
assert not info.filter('invalid_entity_name.somewhere')
else:
assert not info._entities
if domains is not None:
assert info._domains == frozenset(domains)
assert all([info.filter_lifecycle(domain + ".entity")
for domain in domains])
else:
assert not hasattr(info, '_domains')
def test_template_equality():
"""Test template comparison and hashing."""
template_one = template.Template("{{ template_one }}")
template_one_1 = template.Template("{{ template_" + "one }}")
template_two = template.Template("{{ template_two }}")
assert template_one == template_one_1
assert template_one != template_two
assert hash(template_one) == hash(template_one_1)
assert hash(template_one) != hash(template_two)
assert str(template_one_1) == 'Template("{{ template_one }}")'
with pytest.raises(TypeError):
template.Template(["{{ template_one }}"])
def test_invalid_template(hass):
"""Invalid template raises error."""
tmpl = template.Template("{{", hass)
with pytest.raises(TemplateError):
tmpl.ensure_valid()
with pytest.raises(TemplateError):
tmpl.async_render()
info = tmpl.async_render_to_info()
with pytest.raises(TemplateError):
assert info.result == "impossible"
tmpl = template.Template("{{states(keyword)}}", hass)
tmpl.ensure_valid()
with pytest.raises(TemplateError):
tmpl.async_render()
def test_referring_states_by_entity_id(hass):
"""Test referring states by entity id."""
hass.states.async_set('test.object', 'happy')
assert template.Template(
'{{ states.test.object.state }}', hass).async_render() == 'happy'
assert template.Template(
'{{ states["test.object"].state }}',
hass).async_render() == 'happy'
assert template.Template(
'{{ states("test.object") }}', hass).async_render() == 'happy'
def test_invalid_entity_id(hass):
"""Test referring states by entity id."""
with pytest.raises(TemplateError):
template.Template(
'{{ states["big.fat..."] }}', hass).async_render()
with pytest.raises(TemplateError):
template.Template(
'{{ states.test["big.fat..."] }}', hass).async_render()
with pytest.raises(TemplateError):
template.Template(
'{{ states["invalid/domain"] }}', hass).async_render()
def test_raise_exception_on_error(hass):
"""Test raising an exception on error."""
with pytest.raises(TemplateError):
template.Template('{{ invalid_syntax').ensure_valid()
def test_iterating_all_states(hass):
"""Test iterating all states."""
tmpl_str = '{% for state in states %}{{ state.state }}{% endfor %}'
info = render_to_info(hass, tmpl_str)
assert_result_info(info, '', all_states=True)
hass.states.async_set('test.object', 'happy')
hass.states.async_set('sensor.temperature', 10)
assert template.Template(
'{% for state in states %}{{ state.state }}{% endfor %}',
hass).async_render() == '10happy'
info = render_to_info(hass, tmpl_str)
assert_result_info(
info, '10happy',
entities=['test.object', 'sensor.temperature'],
all_states=True)
def test_iterating_domain_states(hass):
"""Test iterating domain states."""
tmpl_str = \
"{% for state in states.sensor %}" \
"{{ state.state }}{% endfor %}"
info = render_to_info(hass, tmpl_str)
assert_result_info(info, '', domains=['sensor'])
hass.states.async_set('test.object', 'happy')
hass.states.async_set('sensor.back_door', 'open')
hass.states.async_set('sensor.temperature', 10)
assert template.Template("""
{% for state in states.sensor %}{{ state.state }}{% endfor %}
""", hass).async_render() == 'open10'
info = render_to_info(hass, tmpl_str)
assert_result_info(
info, 'open10',
entities=['sensor.back_door', 'sensor.temperature'],
domains=['sensor'])
def test_float(hass):
@@ -69,6 +179,10 @@ def test_float(hass):
'{{ float(states.sensor.temperature.state) > 11 }}',
hass).async_render() == 'True'
assert template.Template(
'{{ float(\'forgiving\') }}',
hass).async_render() == 'forgiving'
def test_rounding_value(hass):
"""Test rounding value."""
@@ -140,7 +254,8 @@ def test_sine(hass):
(math.pi / 2, '1.0'),
(math.pi, '0.0'),
(math.pi * 1.5, '-1.0'),
(math.pi / 10, '0.309')
(math.pi / 10, '0.309'),
('"duck"', 'duck'),
]
for value, expected in tests:
@@ -156,7 +271,8 @@ def test_cos(hass):
(math.pi / 2, '0.0'),
(math.pi, '-1.0'),
(math.pi * 1.5, '-0.0'),
(math.pi / 10, '0.951')
(math.pi / 10, '0.951'),
("'error'", 'error'),
]
for value, expected in tests:
@@ -172,7 +288,8 @@ def test_tan(hass):
(math.pi, '-0.0'),
(math.pi / 180 * 45, '1.0'),
(math.pi / 180 * 90, '1.633123935319537e+16'),
(math.pi / 180 * 135, '-1.0')
(math.pi / 180 * 135, '-1.0'),
("'error'", 'error'),
]
for value, expected in tests:
@@ -189,6 +306,7 @@ def test_sqrt(hass):
(2, '1.414'),
(10, '3.162'),
(100, '10.0'),
("'error'", 'error'),
]
for value, expected in tests:
@@ -290,6 +408,9 @@ def test_ordinal(hass):
(3, '3rd'),
(4, '4th'),
(5, '5th'),
(12, '12th'),
(100, '100th'),
(101, '101st'),
]
for value, expected in tests:
@@ -433,12 +554,6 @@ def test_render_with_possible_json_value_non_string_value(hass):
assert tpl.async_render_with_possible_json_value(value) == expected
def test_raise_exception_on_error(hass):
"""Test raising an exception on error."""
with pytest.raises(TemplateError):
template.Template('{{ invalid_syntax').ensure_valid()
def test_if_state_exists(hass):
"""Test if state exists works."""
hass.states.async_set('test.object', 'available')
@@ -539,6 +654,11 @@ def test_regex_match(hass):
""", hass)
assert tpl.async_render() == 'False'
tpl = template.Template("""
{{ ['home assistant test'] | regex_match('.*assist') }}
""", hass)
assert tpl.async_render() == 'True'
def test_regex_search(hass):
"""Test regex_search method."""
@@ -557,6 +677,11 @@ def test_regex_search(hass):
""", hass)
assert tpl.async_render() == 'True'
tpl = template.Template("""
{{ ['home assistant test'] | regex_search('assist') }}
""", hass)
assert tpl.async_render() == 'True'
def test_regex_replace(hass):
"""Test regex_replace method."""
@@ -565,6 +690,11 @@ def test_regex_replace(hass):
""", hass)
assert tpl.async_render() == 'World'
tpl = template.Template("""
{{ ['home hinderant test'] | regex_replace('hinder', 'assist') }}
""", hass)
assert tpl.async_render() == "['home assistant test']"
def test_regex_findall_index(hass):
"""Test regex_findall_index method."""
@@ -578,6 +708,11 @@ def test_regex_findall_index(hass):
""", hass)
assert tpl.async_render() == 'LHR'
tpl = template.Template("""
{{ ['JFK', 'LHR'] | regex_findall_index('([A-Z]{3})', 1) }}
""", hass)
assert tpl.async_render() == 'LHR'
def test_bitwise_and(hass):
"""Test bitwise_and method."""
@@ -779,9 +914,10 @@ async def test_closest_function_home_vs_group_entity_id(hass):
await group.Group.async_create_group(
hass, 'location group', ['test_domain.object'])
assert template.Template(
'{{ closest("group.location_group").entity_id }}',
hass).async_render() == 'test_domain.object'
info = render_to_info(
hass, '{{ closest("group.location_group").entity_id }}')
assert_result_info(info, 'test_domain.object', [
'test_domain.object', 'group.location_group'])
async def test_closest_function_home_vs_group_state(hass):
@@ -799,9 +935,17 @@ async def test_closest_function_home_vs_group_state(hass):
await group.Group.async_create_group(
hass, 'location group', ['test_domain.object'])
assert template.Template(
'{{ closest(states.group.location_group).entity_id }}',
hass).async_render() == 'test_domain.object'
info = render_to_info(
hass, '{{ closest("group.location_group").entity_id }}')
assert_result_info(
info, 'test_domain.object',
['test_domain.object', 'group.location_group'])
info = render_to_info(
hass, '{{ closest(states.group.location_group).entity_id }}')
assert_result_info(
info, 'test_domain.object',
['test_domain.object', 'group.location_group'])
def test_closest_function_to_coord(hass):
@@ -846,10 +990,18 @@ def test_closest_function_to_entity_id(hass):
'longitude': hass.config.longitude + 0.3,
})
assert template.Template(
'{{ closest("zone.far_away", '
'states.test_domain).entity_id }}', hass).async_render() == \
'test_domain.closest_zone'
info = render_to_info(
hass,
'{{ closest(zone, states.test_domain).entity_id }}',
{
'zone': 'zone.far_away'
})
assert_result_info(
info, 'test_domain.closest_zone',
['test_domain.closest_home', 'test_domain.closest_zone',
'zone.far_away'],
["test_domain"])
def test_closest_function_to_state(hass):
@@ -935,11 +1087,83 @@ def test_extract_entities_no_match_entities(hass):
assert template.extract_entities(
"{{ value_json.tst | timestamp_custom('%Y' True) }}") == MATCH_ALL
assert template.extract_entities("""
info = render_to_info(hass, """
{% for state in states.sensor %}
{{ state.entity_id }}={{ state.state }},d
{% endfor %}
""") == MATCH_ALL
""")
assert_result_info(info, '', domains=['sensor'])
def test_generate_filter_iterators(hass):
"""Test extract entities function with none entities stuff."""
info = render_to_info(hass, """
{% for state in states %}
{{ state.entity_id }}
{% endfor %}
""")
assert_result_info(info, '', all_states=True)
info = render_to_info(hass, """
{% for state in states.sensor %}
{{ state.entity_id }}
{% endfor %}
""")
assert_result_info(info, '', domains=['sensor'])
hass.states.async_set('sensor.test_sensor', 'off', {
'attr': 'value'})
# Don't need the entity because the state is not accessed
info = render_to_info(hass, """
{% for state in states.sensor %}
{{ state.entity_id }}
{% endfor %}
""")
assert_result_info(info, 'sensor.test_sensor', domains=['sensor'])
# But we do here because the state gets accessed
info = render_to_info(hass, """
{% for state in states.sensor %}
{{ state.entity_id }}={{ state.state }},
{% endfor %}
""")
assert_result_info(
info, 'sensor.test_sensor=off,',
['sensor.test_sensor'],
['sensor'])
info = render_to_info(hass, """
{% for state in states.sensor %}
{{ state.entity_id }}={{ state.attributes.attr }},
{% endfor %}
""")
assert_result_info(
info, 'sensor.test_sensor=value,',
['sensor.test_sensor'],
['sensor'])
def test_generate_select(hass):
"""Test extract entities function with none entities stuff."""
template_str = """
{{ states.sensor|selectattr("state","equalto","off")
|join(",", attribute="entity_id") }}
"""
tmp = template.Template(template_str, hass)
info = tmp.async_render_to_info()
assert_result_info(info, '', [], ['sensor'])
hass.states.async_set('sensor.test_sensor', 'off', {
'attr': 'value'})
hass.states.async_set('sensor.test_sensor_on', 'on')
info = tmp.async_render_to_info()
assert_result_info(
info, 'sensor.test_sensor',
['sensor.test_sensor', 'sensor.test_sensor_on'],
['sensor'])
def test_extract_entities_match_entities(hass):
@@ -960,6 +1184,10 @@ Hercules is at {{ states('device_tracker.phone_1') }}.
{{ states("binary_sensor.garage_door") }}
""") == ['binary_sensor.garage_door']
hass.states.async_set('device_tracker.phone_2', 'not_home', {
'battery': 20
})
assert template.extract_entities("""
{{ is_state_attr('device_tracker.phone_2', 'battery', 40) }}
""") == ['device_tracker.phone_2']
@@ -1000,30 +1228,42 @@ states.sensor.pick_humidity.state ~ „ %“
def test_extract_entities_with_variables(hass):
"""Test extract entities function with variables and entities stuff."""
assert template.extract_entities(
"{{ is_state('input_boolean.switch', 'off') }}", {}) == \
['input_boolean.switch']
hass.states.async_set('input_boolean.switch', 'on')
assert {'input_boolean.switch'} == \
extract_entities(
hass, "{{ is_state('input_boolean.switch', 'off') }}", {})
assert template.extract_entities(
"{{ is_state(trigger.entity_id, 'off') }}", {}) == \
['trigger.entity_id']
assert {'input_boolean.switch'} == extract_entities(
hass, "{{ is_state(trigger.entity_id, 'off') }}", {
'trigger': {
'entity_id': 'input_boolean.switch'
}
})
assert template.extract_entities(
"{{ is_state(data, 'off') }}", {}) == MATCH_ALL
assert {'no_state'} == extract_entities(
hass,
"{{ is_state(data, 'off') }}", {
'data': 'no_state'
})
assert template.extract_entities(
"{{ is_state(data, 'off') }}",
{'data': 'input_boolean.switch'}) == \
['input_boolean.switch']
assert {'input_boolean.switch'} == \
extract_entities(
hass,
"{{ is_state(data, 'off') }}",
{'data': 'input_boolean.switch'})
assert template.extract_entities(
"{{ is_state(trigger.entity_id, 'off') }}",
{'trigger': {'entity_id': 'input_boolean.switch'}}) == \
['input_boolean.switch']
assert {'input_boolean.switch'} == \
extract_entities(
hass,
"{{ is_state(trigger.entity_id, 'off') }}",
{'trigger': {'entity_id': 'input_boolean.switch'}})
assert template.extract_entities(
"{{ is_state('media_player.' ~ where , 'playing') }}",
{'where': 'livingroom'}) == MATCH_ALL
hass.states.async_set('media_player.livingroom', 'off')
assert {'media_player.livingroom'} == \
extract_entities(
hass,
"{{ is_state('media_player.' ~ where , 'playing') }}",
{'where': 'livingroom'})
def test_jinja_namespace(hass):
@@ -1044,7 +1284,7 @@ def test_jinja_namespace(hass):
assert test_template.async_render() == 'another value'
async def test_state_with_unit(hass):
def test_state_with_unit(hass):
"""Test the state_with_unit property helper."""
hass.states.async_set('sensor.test', '23', {
'unit_of_measurement': 'beers',
@@ -1073,7 +1313,7 @@ async def test_state_with_unit(hass):
assert tpl.async_render() == ''
async def test_length_of_states(hass):
def test_length_of_states(hass):
"""Test fetching the length of states."""
hass.states.async_set('sensor.test', '23')
hass.states.async_set('sensor.test2', 'wow')