Skip to content
Closed
23 changes: 23 additions & 0 deletions homeassistant/helpers/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
):
Expand Down
52 changes: 52 additions & 0 deletions tests/helpers/test_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down