Add template function: combine (#140948)

* Add template function: combine

* Add test to take away concern raised
This commit is contained in:
Franck Nijhof 2025-03-20 10:29:40 +01:00 committed by GitHub
parent 3fb0290fba
commit c6d3928ed1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 64 additions and 0 deletions

View File

@ -2785,6 +2785,32 @@ def flatten(value: Iterable[Any], levels: int | None = None) -> list[Any]:
return flattened
def combine(*args: Any, recursive: bool = False) -> dict[Any, Any]:
"""Combine multiple dictionaries into one."""
if not args:
raise TypeError("combine expected at least 1 argument, got 0")
result: dict[Any, Any] = {}
for arg in args:
if not isinstance(arg, dict):
raise TypeError(f"combine expected a dict, got {type(arg).__name__}")
if recursive:
for key, value in arg.items():
if (
key in result
and isinstance(result[key], dict)
and isinstance(value, dict)
):
result[key] = combine(result[key], value, recursive=True)
else:
result[key] = value
else:
result |= arg
return result
def md5(value: str) -> str:
"""Generate md5 hash from a string."""
return hashlib.md5(value.encode()).hexdigest()
@ -3012,6 +3038,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.filters["sha1"] = sha1
self.filters["sha256"] = sha256
self.filters["sha512"] = sha512
self.filters["combine"] = combine
self.globals["log"] = logarithm
self.globals["sin"] = sine
self.globals["cos"] = cosine
@ -3056,6 +3083,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.globals["sha1"] = sha1
self.globals["sha256"] = sha256
self.globals["sha512"] = sha512
self.globals["combine"] = combine
self.tests["is_number"] = is_number
self.tests["list"] = _is_list
self.tests["set"] = _is_set

View File

@ -6840,3 +6840,39 @@ def test_sha512(hass: HomeAssistant) -> None:
template.Template("{{ 'Home Assistant' | sha512 }}", hass).async_render()
== "9e3c2cdd1fbab0037378d37e1baf8a3a4bf92c54b56ad1d459deee30ccbb2acbebd7a3614552ea08992ad27dedeb7b4c5473525ba90cb73dbe8b9ec5f69295bb"
)
def test_combine(hass: HomeAssistant) -> None:
"""Test combine filter and function."""
assert template.Template(
"{{ {'a': 1, 'b': 2} | combine({'b': 3, 'c': 4}) }}", hass
).async_render() == {"a": 1, "b": 3, "c": 4}
assert template.Template(
"{{ combine({'a': 1, 'b': 2}, {'b': 3, 'c': 4}) }}", hass
).async_render() == {"a": 1, "b": 3, "c": 4}
assert template.Template(
"{{ combine({'a': 1, 'b': {'x': 1}}, {'b': {'y': 2}, 'c': 4}, recursive=True) }}",
hass,
).async_render() == {"a": 1, "b": {"x": 1, "y": 2}, "c": 4}
# Test that recursive=False does not merge nested dictionaries
assert template.Template(
"{{ combine({'a': 1, 'b': {'x': 1}}, {'b': {'y': 2}, 'c': 4}, recursive=False) }}",
hass,
).async_render() == {"a": 1, "b": {"y": 2}, "c": 4}
# Test that None values are handled correctly in recursive merge
assert template.Template(
"{{ combine({'a': 1, 'b': none}, {'b': {'y': 2}, 'c': 4}, recursive=True) }}",
hass,
).async_render() == {"a": 1, "b": {"y": 2}, "c": 4}
with pytest.raises(
TemplateError, match="combine expected at least 1 argument, got 0"
):
template.Template("{{ combine() }}", hass).async_render()
with pytest.raises(TemplateError, match="combine expected a dict, got str"):
template.Template("{{ {'a': 1} | combine('not a dict') }}", hass).async_render()