diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 9aafe53925c9ca..02254e07d4091a 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 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,10 @@ 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") + 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 @@ -2084,6 +2105,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 @@ -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