From 18cb53a35c733bad4f31b64b6ee2f1397e6d9db1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 8 Mar 2023 17:28:53 +0100 Subject: [PATCH] Pass hass instance when validating templates (#89242) * Pass hass instance when validating templates * Update tests * Fix validating templates without hass * Update service tests --- homeassistant/helpers/config_validation.py | 20 +++++- tests/helpers/test_config_validation.py | 73 +++++++++++++++++++++- tests/helpers/test_service.py | 18 +++--- 3 files changed, 97 insertions(+), 14 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 42e1927e09b..0f53c9108c8 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -85,7 +85,12 @@ from homeassistant.const import ( WEEKDAYS, UnitOfTemperature, ) -from homeassistant.core import split_entity_id, valid_entity_id +from homeassistant.core import ( + HomeAssistant, + async_get_hass, + split_entity_id, + valid_entity_id, +) from homeassistant.exceptions import TemplateError from homeassistant.generated import currencies from homeassistant.generated.countries import COUNTRIES @@ -597,7 +602,11 @@ def template(value: Any | None) -> template_helper.Template: if isinstance(value, (list, dict, template_helper.Template)): raise vol.Invalid("template value should be a string") - template_value = template_helper.Template(str(value)) + hass: HomeAssistant | None = None + with contextlib.suppress(LookupError): + hass = async_get_hass() + + template_value = template_helper.Template(str(value), hass) try: template_value.ensure_valid() @@ -615,7 +624,12 @@ def dynamic_template(value: Any | None) -> template_helper.Template: if not template_helper.is_template_string(str(value)): raise vol.Invalid("template value does not contain a dynamic template") - template_value = template_helper.Template(str(value)) + hass: HomeAssistant | None = None + with contextlib.suppress(LookupError): + hass = async_get_hass() + + template_value = template_helper.Template(str(value), hass) + try: template_value.ensure_valid() return template_value diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 6823e9655bd..f1f644a36a7 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -561,11 +561,16 @@ def test_x10_address() -> None: schema("C11") -def test_template() -> None: +def test_template(hass: HomeAssistant) -> None: """Test template validator.""" schema = vol.Schema(cv.template) - for value in (None, "{{ partial_print }", "{% if True %}Hello", ["test"]): + for value in ( + None, + "{{ partial_print }", + "{% if True %}Hello", + ["test"], + ): with pytest.raises(vol.Invalid): schema(value) @@ -574,12 +579,43 @@ def test_template() -> None: "Hello", "{{ beer }}", "{% if 1 == 1 %}Hello{% else %}World{% endif %}", + # Function added as an extension by Home Assistant + "{{ expand('group.foo')|map(attribute='entity_id')|list }}", + # Filter added as an extension by Home Assistant + "{{ ['group.foo']|expand|map(attribute='entity_id')|list }}", ) for value in options: schema(value) -def test_dynamic_template() -> None: +async def test_template_no_hass(hass: HomeAssistant) -> None: + """Test template validator.""" + schema = vol.Schema(cv.template) + + for value in ( + None, + "{{ partial_print }", + "{% if True %}Hello", + ["test"], + # Filter added as an extension by Home Assistant + "{{ ['group.foo']|expand|map(attribute='entity_id')|list }}", + ): + with pytest.raises(vol.Invalid): + await hass.async_add_executor_job(schema, value) + + options = ( + 1, + "Hello", + "{{ beer }}", + "{% if 1 == 1 %}Hello{% else %}World{% endif %}", + # Function added as an extension by Home Assistant + "{{ expand('group.foo')|map(attribute='entity_id')|list }}", + ) + for value in options: + await hass.async_add_executor_job(schema, value) + + +def test_dynamic_template(hass: HomeAssistant) -> None: """Test dynamic template validator.""" schema = vol.Schema(cv.dynamic_template) @@ -597,11 +633,42 @@ def test_dynamic_template() -> None: options = ( "{{ beer }}", "{% if 1 == 1 %}Hello{% else %}World{% endif %}", + # Function added as an extension by Home Assistant + "{{ expand('group.foo')|map(attribute='entity_id')|list }}", + # Filter added as an extension by Home Assistant + "{{ ['group.foo']|expand|map(attribute='entity_id')|list }}", ) for value in options: schema(value) +async def test_dynamic_template_no_hass(hass: HomeAssistant) -> None: + """Test dynamic template validator.""" + schema = vol.Schema(cv.dynamic_template) + + for value in ( + None, + 1, + "{{ partial_print }", + "{% if True %}Hello", + ["test"], + "just a string", + # Filter added as an extension by Home Assistant + "{{ ['group.foo']|expand|map(attribute='entity_id')|list }}", + ): + with pytest.raises(vol.Invalid): + await hass.async_add_executor_job(schema, value) + + options = ( + "{{ beer }}", + "{% if 1 == 1 %}Hello{% else %}World{% endif %}", + # Function added as an extension by Home Assistant + "{{ expand('group.foo')|map(attribute='entity_id')|list }}", + ) + for value in options: + await hass.async_add_executor_job(schema, value) + + def test_template_complex() -> None: """Test template_complex validator.""" schema = vol.Schema(cv.template_complex) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index c3b5165bb14..43bbf85b06c 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -383,16 +383,18 @@ async def test_split_entity_string(hass: HomeAssistant): async def test_not_mutate_input(hass: HomeAssistant): """Test for immutable input.""" async_mock_service(hass, "test_domain", "test_service") - config = cv.SERVICE_SCHEMA( - { - "service": "test_domain.test_service", - "entity_id": "hello.world, sensor.beer", - "data": {"hello": 1}, - "data_template": {"nested": {"value": "{{ 1 + 1 }}"}}, - } - ) + config = { + "service": "test_domain.test_service", + "entity_id": "hello.world, sensor.beer", + "data": {"hello": 1}, + "data_template": {"nested": {"value": "{{ 1 + 1 }}"}}, + } orig = deepcopy(config) + # Validate both the original and the copy + config = cv.SERVICE_SCHEMA(config) + orig = cv.SERVICE_SCHEMA(orig) + # Only change after call is each template getting hass attached template.attach(hass, orig)