From f463b09a6de7a7826adf6b35b871ccad90a8a350 Mon Sep 17 00:00:00 2001 From: David Poll Date: Wed, 22 Feb 2023 00:32:01 +0000 Subject: [PATCH 1/3] Enables jinja features and mutability. Turns on as_mutable, expression statements, for loop controls, and include/import --- homeassistant/helpers/template.py | 26 +++++++++++++++++ tests/helpers/test_template.py | 48 +++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 9aafe53925c9ca..732b03c239790f 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1917,6 +1917,23 @@ def to_json(value, ensure_ascii=True): return json.dumps(value, ensure_ascii=ensure_ascii) +class _MutableDict(dict): + pass + + +class _MutableList(list): + pass + + +def as_mutable(value): + """Create a mutable copy of a dict, set, or list.""" + if isinstance(value, dict): + return _MutableDict(value) + if isinstance(value, list): + return _MutableList(value) + raise ValueError("Can only create mutable lists or dicts.") + + @pass_context def random_every_time(context, values): """Choose a random value. @@ -2063,6 +2080,8 @@ def __init__(self, hass, limited=False, strict=False): self.template_cache: weakref.WeakValueDictionary[ str | jinja2.nodes.Template, CodeType | str | None ] = weakref.WeakValueDictionary() + self.add_extension("jinja2.ext.do") + self.add_extension("jinja2.ext.loopcontrols") self.filters["round"] = forgiving_round self.filters["multiply"] = multiply self.filters["log"] = logarithm @@ -2084,6 +2103,7 @@ def __init__(self, hass, limited=False, strict=False): self.filters["timestamp_utc"] = timestamp_utc self.filters["to_json"] = to_json self.filters["from_json"] = from_json + self.filters["as_mutable"] = as_mutable self.filters["is_defined"] = fail_when_undefined self.filters["average"] = average self.filters["random"] = random_every_time @@ -2231,6 +2251,8 @@ def warn_unsupported(*args: Any, **kwargs: Any) -> NoReturn: self.filters[filt] = unsupported(filt) return + self.loader = jinja2.FileSystemLoader(hass.config.path("custom_jinja")) + self.globals["expand"] = hassfunction(expand) self.filters["expand"] = pass_context(self.globals["expand"]) self.globals["closest"] = hassfunction(closest) @@ -2253,6 +2275,10 @@ def is_safe_callable(self, obj): def is_safe_attribute(self, obj, attr, value): """Test if attribute is safe.""" + + if isinstance(obj, (_MutableDict, _MutableList)): + return True + if isinstance( obj, (AllStates, DomainStates, TemplateState, LoopContext, AsyncLoopContext) ): diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index ec6714bafe140d..f51c0a29b96563 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -993,6 +993,54 @@ def test_from_json(hass: HomeAssistant) -> None: assert actual_result == expected_result +def test_mutables(hass: HomeAssistant) -> None: + """Test as_mutable and mutability of those objects.""" + assert ( + template.Template( + """ + {% set d = {} | as_mutable %} + {% do d.update({'foo': 'bar'}) %} + {{ d.foo }} + """, + hass, + ).async_render() + == "bar" + ) + + assert ( + template.Template( + """ + {% set l = [] | as_mutable %} + {% do l.append('bar') %} + {{ l[0] }} + """, + hass, + ).async_render() + == "bar" + ) + + # Ensure that without as_mutable, updates still don't work. + with pytest.raises(TemplateError): + template.Template( + """ + {% set d = {} %} + {% do d.update({'foo': 'bar'}) %} + {{ d.foo }} + """, + hass, + ).async_render() + + with pytest.raises(TemplateError): + template.Template( + """ + {% set l = [] %} + {% do l.append('bar') %} + {{ l[0] }} + """, + hass, + ).async_render() + + def test_average(hass: HomeAssistant) -> None: """Test the average filter.""" assert template.Template("{{ [1, 2, 3] | average }}", hass).async_render() == 2 From 3a8fb0fd347b7d528495fbaace358a8454c414ed Mon Sep 17 00:00:00 2001 From: David Poll Date: Wed, 22 Feb 2023 00:32:41 +0000 Subject: [PATCH 2/3] Fix errant comment. --- homeassistant/helpers/template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 732b03c239790f..19968e61675198 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1926,7 +1926,7 @@ class _MutableList(list): def as_mutable(value): - """Create a mutable copy of a dict, set, or list.""" + """Create a mutable copy of a dict or list.""" if isinstance(value, dict): return _MutableDict(value) if isinstance(value, list): From 0013fb5019db8a87ebf00ae0b0007ddb07dddeed Mon Sep 17 00:00:00 2001 From: David Poll Date: Wed, 22 Feb 2023 00:44:43 +0000 Subject: [PATCH 3/3] Reorder code --- homeassistant/helpers/template.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 19968e61675198..02254e07d4091a 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -2082,6 +2082,8 @@ def __init__(self, hass, limited=False, strict=False): ] = weakref.WeakValueDictionary() self.add_extension("jinja2.ext.do") self.add_extension("jinja2.ext.loopcontrols") + if hass is not None: + self.loader = jinja2.FileSystemLoader(hass.config.path("custom_jinja")) self.filters["round"] = forgiving_round self.filters["multiply"] = multiply self.filters["log"] = logarithm @@ -2251,8 +2253,6 @@ def warn_unsupported(*args: Any, **kwargs: Any) -> NoReturn: self.filters[filt] = unsupported(filt) return - self.loader = jinja2.FileSystemLoader(hass.config.path("custom_jinja")) - self.globals["expand"] = hassfunction(expand) self.filters["expand"] = pass_context(self.globals["expand"]) self.globals["closest"] = hassfunction(closest)