diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 2c2e5b2d95eb37..1d83bc41dcfd0c 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -2034,6 +2034,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. @@ -2241,6 +2258,7 @@ def __init__(self, hass, limited=False, strict=False): str | jinja2.nodes.Template, CodeType | str | None ] = weakref.WeakValueDictionary() self.add_extension("jinja2.ext.loopcontrols") + self.add_extension("jinja2.ext.do") self.filters["round"] = forgiving_round self.filters["multiply"] = multiply self.filters["log"] = logarithm @@ -2261,6 +2279,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 @@ -2463,6 +2482,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 214a349d60388e..c8fe38297520c5 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1075,6 +1075,58 @@ 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() + + # Ensure that as_mutable raises an error when non-dicts/lists are passed + with pytest.raises(TemplateError): + template.Template("{{ 5 | as_mutable }}", hass).async_render() + + def test_average(hass: HomeAssistant) -> None: """Test the average filter.""" assert template.Template("{{ [1, 2, 3] | average }}", hass).async_render() == 2