Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions homeassistant/helpers/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
):
Expand Down
48 changes: 48 additions & 0 deletions tests/helpers/test_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down