mirror of
https://github.com/home-assistant/core.git
synced 2026-04-21 00:49:54 +02:00
Refactor template engine: Extract collection & data structure functions into CollectionExtension (#152446)
This commit is contained in:
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from ast import literal_eval
|
||||
import asyncio
|
||||
import collections.abc
|
||||
from collections.abc import Callable, Generator, Iterable, MutableSequence
|
||||
from collections.abc import Callable, Generator, Iterable
|
||||
from contextlib import AbstractContextManager
|
||||
from contextvars import ContextVar
|
||||
from copy import deepcopy
|
||||
@@ -2245,31 +2245,6 @@ def is_number(value):
|
||||
return True
|
||||
|
||||
|
||||
def _is_list(value: Any) -> bool:
|
||||
"""Return whether a value is a list."""
|
||||
return isinstance(value, list)
|
||||
|
||||
|
||||
def _is_set(value: Any) -> bool:
|
||||
"""Return whether a value is a set."""
|
||||
return isinstance(value, set)
|
||||
|
||||
|
||||
def _is_tuple(value: Any) -> bool:
|
||||
"""Return whether a value is a tuple."""
|
||||
return isinstance(value, tuple)
|
||||
|
||||
|
||||
def _to_set(value: Any) -> set[Any]:
|
||||
"""Convert value to set."""
|
||||
return set(value)
|
||||
|
||||
|
||||
def _to_tuple(value):
|
||||
"""Convert value to tuple."""
|
||||
return tuple(value)
|
||||
|
||||
|
||||
def _is_datetime(value: Any) -> bool:
|
||||
"""Return whether a value is a datetime."""
|
||||
return isinstance(value, datetime)
|
||||
@@ -2487,98 +2462,11 @@ def iif(
|
||||
return if_false
|
||||
|
||||
|
||||
def shuffle(*args: Any, seed: Any = None) -> MutableSequence[Any]:
|
||||
"""Shuffle a list, either with a seed or without."""
|
||||
if not args:
|
||||
raise TypeError("shuffle expected at least 1 argument, got 0")
|
||||
|
||||
# If first argument is iterable and more than 1 argument provided
|
||||
# but not a named seed, then use 2nd argument as seed.
|
||||
if isinstance(args[0], Iterable):
|
||||
items = list(args[0])
|
||||
if len(args) > 1 and seed is None:
|
||||
seed = args[1]
|
||||
elif len(args) == 1:
|
||||
raise TypeError(f"'{type(args[0]).__name__}' object is not iterable")
|
||||
else:
|
||||
items = list(args)
|
||||
|
||||
if seed:
|
||||
r = random.Random(seed)
|
||||
r.shuffle(items)
|
||||
else:
|
||||
random.shuffle(items)
|
||||
return items
|
||||
|
||||
|
||||
def typeof(value: Any) -> Any:
|
||||
"""Return the type of value passed to debug types."""
|
||||
return value.__class__.__name__
|
||||
|
||||
|
||||
def flatten(value: Iterable[Any], levels: int | None = None) -> list[Any]:
|
||||
"""Flattens list of lists."""
|
||||
if not isinstance(value, Iterable) or isinstance(value, str):
|
||||
raise TypeError(f"flatten expected a list, got {type(value).__name__}")
|
||||
|
||||
flattened: list[Any] = []
|
||||
for item in value:
|
||||
if isinstance(item, Iterable) and not isinstance(item, str):
|
||||
if levels is None:
|
||||
flattened.extend(flatten(item))
|
||||
elif levels >= 1:
|
||||
flattened.extend(flatten(item, levels=(levels - 1)))
|
||||
else:
|
||||
flattened.append(item)
|
||||
else:
|
||||
flattened.append(item)
|
||||
return flattened
|
||||
|
||||
|
||||
def intersect(value: Iterable[Any], other: Iterable[Any]) -> list[Any]:
|
||||
"""Return the common elements between two lists."""
|
||||
if not isinstance(value, Iterable) or isinstance(value, str):
|
||||
raise TypeError(f"intersect expected a list, got {type(value).__name__}")
|
||||
if not isinstance(other, Iterable) or isinstance(other, str):
|
||||
raise TypeError(f"intersect expected a list, got {type(other).__name__}")
|
||||
|
||||
return list(set(value) & set(other))
|
||||
|
||||
|
||||
def difference(value: Iterable[Any], other: Iterable[Any]) -> list[Any]:
|
||||
"""Return elements in first list that are not in second list."""
|
||||
if not isinstance(value, Iterable) or isinstance(value, str):
|
||||
raise TypeError(f"difference expected a list, got {type(value).__name__}")
|
||||
if not isinstance(other, Iterable) or isinstance(other, str):
|
||||
raise TypeError(f"difference expected a list, got {type(other).__name__}")
|
||||
|
||||
return list(set(value) - set(other))
|
||||
|
||||
|
||||
def union(value: Iterable[Any], other: Iterable[Any]) -> list[Any]:
|
||||
"""Return all unique elements from both lists combined."""
|
||||
if not isinstance(value, Iterable) or isinstance(value, str):
|
||||
raise TypeError(f"union expected a list, got {type(value).__name__}")
|
||||
if not isinstance(other, Iterable) or isinstance(other, str):
|
||||
raise TypeError(f"union expected a list, got {type(other).__name__}")
|
||||
|
||||
return list(set(value) | set(other))
|
||||
|
||||
|
||||
def symmetric_difference(value: Iterable[Any], other: Iterable[Any]) -> list[Any]:
|
||||
"""Return elements that are in either list but not in both."""
|
||||
if not isinstance(value, Iterable) or isinstance(value, str):
|
||||
raise TypeError(
|
||||
f"symmetric_difference expected a list, got {type(value).__name__}"
|
||||
)
|
||||
if not isinstance(other, Iterable) or isinstance(other, str):
|
||||
raise TypeError(
|
||||
f"symmetric_difference expected a list, got {type(other).__name__}"
|
||||
)
|
||||
|
||||
return list(set(value) ^ set(other))
|
||||
|
||||
|
||||
def combine(*args: Any, recursive: bool = False) -> dict[Any, Any]:
|
||||
"""Combine multiple dictionaries into one."""
|
||||
if not args:
|
||||
@@ -2760,11 +2648,15 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||
self.add_extension("jinja2.ext.loopcontrols")
|
||||
self.add_extension("jinja2.ext.do")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.Base64Extension")
|
||||
self.add_extension(
|
||||
"homeassistant.helpers.template.extensions.CollectionExtension"
|
||||
)
|
||||
self.add_extension("homeassistant.helpers.template.extensions.CryptoExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.MathExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.RegexExtension")
|
||||
self.add_extension("homeassistant.helpers.template.extensions.StringExtension")
|
||||
|
||||
self.globals["apply"] = apply
|
||||
self.globals["as_datetime"] = as_datetime
|
||||
self.globals["as_function"] = as_function
|
||||
self.globals["as_local"] = dt_util.as_local
|
||||
@@ -2772,23 +2664,15 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||
self.globals["as_timestamp"] = forgiving_as_timestamp
|
||||
self.globals["bool"] = forgiving_boolean
|
||||
self.globals["combine"] = combine
|
||||
self.globals["difference"] = difference
|
||||
self.globals["flatten"] = flatten
|
||||
self.globals["float"] = forgiving_float
|
||||
self.globals["iif"] = iif
|
||||
self.globals["int"] = forgiving_int
|
||||
self.globals["intersect"] = intersect
|
||||
self.globals["is_number"] = is_number
|
||||
self.globals["merge_response"] = merge_response
|
||||
self.globals["pack"] = struct_pack
|
||||
self.globals["set"] = _to_set
|
||||
self.globals["shuffle"] = shuffle
|
||||
self.globals["strptime"] = strptime
|
||||
self.globals["symmetric_difference"] = symmetric_difference
|
||||
self.globals["timedelta"] = timedelta
|
||||
self.globals["tuple"] = _to_tuple
|
||||
self.globals["typeof"] = typeof
|
||||
self.globals["union"] = union
|
||||
self.globals["unpack"] = struct_unpack
|
||||
self.globals["version"] = version
|
||||
self.globals["zip"] = zip
|
||||
@@ -2803,14 +2687,11 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||
self.filters["bool"] = forgiving_boolean
|
||||
self.filters["combine"] = combine
|
||||
self.filters["contains"] = contains
|
||||
self.filters["difference"] = difference
|
||||
self.filters["flatten"] = flatten
|
||||
self.filters["float"] = forgiving_float_filter
|
||||
self.filters["from_json"] = from_json
|
||||
self.filters["from_hex"] = from_hex
|
||||
self.filters["iif"] = iif
|
||||
self.filters["int"] = forgiving_int_filter
|
||||
self.filters["intersect"] = intersect
|
||||
self.filters["is_defined"] = fail_when_undefined
|
||||
self.filters["is_number"] = is_number
|
||||
self.filters["multiply"] = multiply
|
||||
@@ -2818,14 +2699,11 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||
self.filters["pack"] = struct_pack
|
||||
self.filters["random"] = random_every_time
|
||||
self.filters["round"] = forgiving_round
|
||||
self.filters["shuffle"] = shuffle
|
||||
self.filters["symmetric_difference"] = symmetric_difference
|
||||
self.filters["timestamp_custom"] = timestamp_custom
|
||||
self.filters["timestamp_local"] = timestamp_local
|
||||
self.filters["timestamp_utc"] = timestamp_utc
|
||||
self.filters["to_json"] = to_json
|
||||
self.filters["typeof"] = typeof
|
||||
self.filters["union"] = union
|
||||
self.filters["unpack"] = struct_unpack
|
||||
self.filters["version"] = version
|
||||
|
||||
@@ -2833,10 +2711,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||
self.tests["contains"] = contains
|
||||
self.tests["datetime"] = _is_datetime
|
||||
self.tests["is_number"] = is_number
|
||||
self.tests["list"] = _is_list
|
||||
self.tests["set"] = _is_set
|
||||
self.tests["string_like"] = _is_string_like
|
||||
self.tests["tuple"] = _is_tuple
|
||||
|
||||
if hass is None:
|
||||
return
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Home Assistant template extensions."""
|
||||
|
||||
from .base64 import Base64Extension
|
||||
from .collection import CollectionExtension
|
||||
from .crypto import CryptoExtension
|
||||
from .math import MathExtension
|
||||
from .regex import RegexExtension
|
||||
@@ -8,6 +9,7 @@ from .string import StringExtension
|
||||
|
||||
__all__ = [
|
||||
"Base64Extension",
|
||||
"CollectionExtension",
|
||||
"CryptoExtension",
|
||||
"MathExtension",
|
||||
"RegexExtension",
|
||||
|
||||
191
homeassistant/helpers/template/extensions/collection.py
Normal file
191
homeassistant/helpers/template/extensions/collection.py
Normal file
@@ -0,0 +1,191 @@
|
||||
"""Collection and data structure functions for Home Assistant templates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable, MutableSequence
|
||||
import random
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from .base import BaseTemplateExtension, TemplateFunction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.helpers.template import TemplateEnvironment
|
||||
|
||||
|
||||
class CollectionExtension(BaseTemplateExtension):
|
||||
"""Extension for collection and data structure operations."""
|
||||
|
||||
def __init__(self, environment: TemplateEnvironment) -> None:
|
||||
"""Initialize the collection extension."""
|
||||
super().__init__(
|
||||
environment,
|
||||
functions=[
|
||||
TemplateFunction(
|
||||
"flatten",
|
||||
self.flatten,
|
||||
as_global=True,
|
||||
as_filter=True,
|
||||
),
|
||||
TemplateFunction(
|
||||
"shuffle",
|
||||
self.shuffle,
|
||||
as_global=True,
|
||||
as_filter=True,
|
||||
),
|
||||
# Set operations
|
||||
TemplateFunction(
|
||||
"intersect",
|
||||
self.intersect,
|
||||
as_global=True,
|
||||
as_filter=True,
|
||||
),
|
||||
TemplateFunction(
|
||||
"difference",
|
||||
self.difference,
|
||||
as_global=True,
|
||||
as_filter=True,
|
||||
),
|
||||
TemplateFunction(
|
||||
"union",
|
||||
self.union,
|
||||
as_global=True,
|
||||
as_filter=True,
|
||||
),
|
||||
TemplateFunction(
|
||||
"symmetric_difference",
|
||||
self.symmetric_difference,
|
||||
as_global=True,
|
||||
as_filter=True,
|
||||
),
|
||||
# Type conversion functions
|
||||
TemplateFunction(
|
||||
"set",
|
||||
self.to_set,
|
||||
as_global=True,
|
||||
),
|
||||
TemplateFunction(
|
||||
"tuple",
|
||||
self.to_tuple,
|
||||
as_global=True,
|
||||
),
|
||||
# Type checking functions (tests)
|
||||
TemplateFunction(
|
||||
"list",
|
||||
self.is_list,
|
||||
as_test=True,
|
||||
),
|
||||
TemplateFunction(
|
||||
"set",
|
||||
self.is_set,
|
||||
as_test=True,
|
||||
),
|
||||
TemplateFunction(
|
||||
"tuple",
|
||||
self.is_tuple,
|
||||
as_test=True,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def flatten(self, value: Iterable[Any], levels: int | None = None) -> list[Any]:
|
||||
"""Flatten list of lists."""
|
||||
if not isinstance(value, Iterable) or isinstance(value, str):
|
||||
raise TypeError(f"flatten expected a list, got {type(value).__name__}")
|
||||
|
||||
flattened: list[Any] = []
|
||||
for item in value:
|
||||
if isinstance(item, Iterable) and not isinstance(item, str):
|
||||
if levels is None:
|
||||
flattened.extend(self.flatten(item))
|
||||
elif levels >= 1:
|
||||
flattened.extend(self.flatten(item, levels=(levels - 1)))
|
||||
else:
|
||||
flattened.append(item)
|
||||
else:
|
||||
flattened.append(item)
|
||||
return flattened
|
||||
|
||||
def shuffle(self, *args: Any, seed: Any = None) -> MutableSequence[Any]:
|
||||
"""Shuffle a list, either with a seed or without."""
|
||||
if not args:
|
||||
raise TypeError("shuffle expected at least 1 argument, got 0")
|
||||
|
||||
# If first argument is iterable and more than 1 argument provided
|
||||
# but not a named seed, then use 2nd argument as seed.
|
||||
if isinstance(args[0], Iterable) and not isinstance(args[0], str):
|
||||
items = list(args[0])
|
||||
if len(args) > 1 and seed is None:
|
||||
seed = args[1]
|
||||
elif len(args) == 1:
|
||||
raise TypeError(f"'{type(args[0]).__name__}' object is not iterable")
|
||||
else:
|
||||
items = list(args)
|
||||
|
||||
if seed:
|
||||
r = random.Random(seed)
|
||||
r.shuffle(items)
|
||||
else:
|
||||
random.shuffle(items)
|
||||
return items
|
||||
|
||||
def intersect(self, value: Iterable[Any], other: Iterable[Any]) -> list[Any]:
|
||||
"""Return the common elements between two lists."""
|
||||
if not isinstance(value, Iterable) or isinstance(value, str):
|
||||
raise TypeError(f"intersect expected a list, got {type(value).__name__}")
|
||||
if not isinstance(other, Iterable) or isinstance(other, str):
|
||||
raise TypeError(f"intersect expected a list, got {type(other).__name__}")
|
||||
|
||||
return list(set(value) & set(other))
|
||||
|
||||
def difference(self, value: Iterable[Any], other: Iterable[Any]) -> list[Any]:
|
||||
"""Return elements in first list that are not in second list."""
|
||||
if not isinstance(value, Iterable) or isinstance(value, str):
|
||||
raise TypeError(f"difference expected a list, got {type(value).__name__}")
|
||||
if not isinstance(other, Iterable) or isinstance(other, str):
|
||||
raise TypeError(f"difference expected a list, got {type(other).__name__}")
|
||||
|
||||
return list(set(value) - set(other))
|
||||
|
||||
def union(self, value: Iterable[Any], other: Iterable[Any]) -> list[Any]:
|
||||
"""Return all unique elements from both lists combined."""
|
||||
if not isinstance(value, Iterable) or isinstance(value, str):
|
||||
raise TypeError(f"union expected a list, got {type(value).__name__}")
|
||||
if not isinstance(other, Iterable) or isinstance(other, str):
|
||||
raise TypeError(f"union expected a list, got {type(other).__name__}")
|
||||
|
||||
return list(set(value) | set(other))
|
||||
|
||||
def symmetric_difference(
|
||||
self, value: Iterable[Any], other: Iterable[Any]
|
||||
) -> list[Any]:
|
||||
"""Return elements that are in either list but not in both."""
|
||||
if not isinstance(value, Iterable) or isinstance(value, str):
|
||||
raise TypeError(
|
||||
f"symmetric_difference expected a list, got {type(value).__name__}"
|
||||
)
|
||||
if not isinstance(other, Iterable) or isinstance(other, str):
|
||||
raise TypeError(
|
||||
f"symmetric_difference expected a list, got {type(other).__name__}"
|
||||
)
|
||||
|
||||
return list(set(value) ^ set(other))
|
||||
|
||||
def to_set(self, value: Any) -> set[Any]:
|
||||
"""Convert value to set."""
|
||||
return set(value)
|
||||
|
||||
def to_tuple(self, value: Any) -> tuple[Any, ...]:
|
||||
"""Convert value to tuple."""
|
||||
return tuple(value)
|
||||
|
||||
def is_list(self, value: Any) -> bool:
|
||||
"""Return whether a value is a list."""
|
||||
return isinstance(value, list)
|
||||
|
||||
def is_set(self, value: Any) -> bool:
|
||||
"""Return whether a value is a set."""
|
||||
return isinstance(value, set)
|
||||
|
||||
def is_tuple(self, value: Any) -> bool:
|
||||
"""Return whether a value is a tuple."""
|
||||
return isinstance(value, tuple)
|
||||
357
tests/helpers/template/extensions/test_collection.py
Normal file
357
tests/helpers/template/extensions/test_collection.py
Normal file
@@ -0,0 +1,357 @@
|
||||
"""Test collection extension."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import template
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
([1, 2, 3], True),
|
||||
({"a": 1}, False),
|
||||
({1, 2, 3}, False),
|
||||
((1, 2, 3), False),
|
||||
("abc", False),
|
||||
("", False),
|
||||
(5, False),
|
||||
(None, False),
|
||||
({"foo": "bar", "baz": "qux"}, False),
|
||||
],
|
||||
)
|
||||
def test_is_list(hass: HomeAssistant, value: Any, expected: bool) -> None:
|
||||
"""Test list test."""
|
||||
assert (
|
||||
template.Template("{{ value is list }}", hass).async_render({"value": value})
|
||||
== expected
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
([1, 2, 3], False),
|
||||
({"a": 1}, False),
|
||||
({1, 2, 3}, True),
|
||||
((1, 2, 3), False),
|
||||
("abc", False),
|
||||
("", False),
|
||||
(5, False),
|
||||
(None, False),
|
||||
({"foo": "bar", "baz": "qux"}, False),
|
||||
],
|
||||
)
|
||||
def test_is_set(hass: HomeAssistant, value: Any, expected: bool) -> None:
|
||||
"""Test set test."""
|
||||
assert (
|
||||
template.Template("{{ value is set }}", hass).async_render({"value": value})
|
||||
== expected
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
([1, 2, 3], False),
|
||||
({"a": 1}, False),
|
||||
({1, 2, 3}, False),
|
||||
((1, 2, 3), True),
|
||||
("abc", False),
|
||||
("", False),
|
||||
(5, False),
|
||||
(None, False),
|
||||
({"foo": "bar", "baz": "qux"}, False),
|
||||
],
|
||||
)
|
||||
def test_is_tuple(hass: HomeAssistant, value: Any, expected: bool) -> None:
|
||||
"""Test tuple test."""
|
||||
assert (
|
||||
template.Template("{{ value is tuple }}", hass).async_render({"value": value})
|
||||
== expected
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
([1, 2, 3], {"expected0": {1, 2, 3}}),
|
||||
({"a": 1}, {"expected1": {"a"}}),
|
||||
({1, 2, 3}, {"expected2": {1, 2, 3}}),
|
||||
((1, 2, 3), {"expected3": {1, 2, 3}}),
|
||||
("abc", {"expected4": {"a", "b", "c"}}),
|
||||
("", {"expected5": set()}),
|
||||
(range(3), {"expected6": {0, 1, 2}}),
|
||||
({"foo": "bar", "baz": "qux"}, {"expected7": {"foo", "baz"}}),
|
||||
],
|
||||
)
|
||||
def test_set(hass: HomeAssistant, value: Any, expected: bool) -> None:
|
||||
"""Test set conversion."""
|
||||
assert (
|
||||
template.Template("{{ set(value) }}", hass).async_render({"value": value})
|
||||
== list(expected.values())[0]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
([1, 2, 3], {"expected0": (1, 2, 3)}),
|
||||
({"a": 1}, {"expected1": ("a",)}),
|
||||
({1, 2, 3}, {"expected2": (1, 2, 3)}), # Note: set order is not guaranteed
|
||||
((1, 2, 3), {"expected3": (1, 2, 3)}),
|
||||
("abc", {"expected4": ("a", "b", "c")}),
|
||||
("", {"expected5": ()}),
|
||||
(range(3), {"expected6": (0, 1, 2)}),
|
||||
({"foo": "bar", "baz": "qux"}, {"expected7": ("foo", "baz")}),
|
||||
],
|
||||
)
|
||||
def test_tuple(hass: HomeAssistant, value: Any, expected: bool) -> None:
|
||||
"""Test tuple conversion."""
|
||||
result = template.Template("{{ tuple(value) }}", hass).async_render(
|
||||
{"value": value}
|
||||
)
|
||||
expected_value = list(expected.values())[0]
|
||||
if isinstance(value, set): # Sets don't have predictable order
|
||||
assert set(result) == set(expected_value)
|
||||
else:
|
||||
assert result == expected_value
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("cola", "colb", "expected"),
|
||||
[
|
||||
([1, 2], [3, 4], [(1, 3), (2, 4)]),
|
||||
([1, 2], [3, 4, 5], [(1, 3), (2, 4)]),
|
||||
([1, 2, 3, 4], [3, 4], [(1, 3), (2, 4)]),
|
||||
],
|
||||
)
|
||||
def test_zip(hass: HomeAssistant, cola, colb, expected) -> None:
|
||||
"""Test zip."""
|
||||
assert (
|
||||
template.Template("{{ zip(cola, colb) | list }}", hass).async_render(
|
||||
{"cola": cola, "colb": colb}
|
||||
)
|
||||
== expected
|
||||
)
|
||||
assert (
|
||||
template.Template(
|
||||
"[{% for a, b in zip(cola, colb) %}({{a}}, {{b}}), {% endfor %}]", hass
|
||||
).async_render({"cola": cola, "colb": colb})
|
||||
== expected
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("col", "expected"),
|
||||
[
|
||||
([(1, 3), (2, 4)], [(1, 2), (3, 4)]),
|
||||
(["ax", "by", "cz"], [("a", "b", "c"), ("x", "y", "z")]),
|
||||
],
|
||||
)
|
||||
def test_unzip(hass: HomeAssistant, col, expected) -> None:
|
||||
"""Test unzipping using zip."""
|
||||
assert (
|
||||
template.Template("{{ zip(*col) | list }}", hass).async_render({"col": col})
|
||||
== expected
|
||||
)
|
||||
assert (
|
||||
template.Template(
|
||||
"{% set a, b = zip(*col) %}[{{a}}, {{b}}]", hass
|
||||
).async_render({"col": col})
|
||||
== expected
|
||||
)
|
||||
|
||||
|
||||
def test_shuffle(hass: HomeAssistant) -> None:
|
||||
"""Test shuffle."""
|
||||
# Test basic shuffle
|
||||
result = template.Template("{{ shuffle([1, 2, 3, 4, 5]) }}", hass).async_render()
|
||||
assert len(result) == 5
|
||||
assert set(result) == {1, 2, 3, 4, 5}
|
||||
|
||||
# Test shuffle with seed
|
||||
result1 = template.Template(
|
||||
"{{ shuffle([1, 2, 3, 4, 5], seed=42) }}", hass
|
||||
).async_render()
|
||||
result2 = template.Template(
|
||||
"{{ shuffle([1, 2, 3, 4, 5], seed=42) }}", hass
|
||||
).async_render()
|
||||
assert result1 == result2 # Same seed should give same result
|
||||
|
||||
# Test shuffle with different seed
|
||||
result3 = template.Template(
|
||||
"{{ shuffle([1, 2, 3, 4, 5], seed=123) }}", hass
|
||||
).async_render()
|
||||
# Different seeds should usually give different results
|
||||
# (but we can't guarantee it for small lists)
|
||||
assert len(result3) == 5
|
||||
assert set(result3) == {1, 2, 3, 4, 5}
|
||||
|
||||
|
||||
def test_flatten(hass: HomeAssistant) -> None:
|
||||
"""Test flatten."""
|
||||
# Test basic flattening
|
||||
assert template.Template(
|
||||
"{{ flatten([[1, 2], [3, 4]]) }}", hass
|
||||
).async_render() == [1, 2, 3, 4]
|
||||
|
||||
# Test nested flattening
|
||||
assert template.Template(
|
||||
"{{ flatten([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) }}", hass
|
||||
).async_render() == [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
|
||||
# Test flattening with levels
|
||||
assert template.Template(
|
||||
"{{ flatten([[[1, 2], [3, 4]], [[5, 6], [7, 8]]], levels=1) }}", hass
|
||||
).async_render() == [[1, 2], [3, 4], [5, 6], [7, 8]]
|
||||
|
||||
# Test mixed types
|
||||
assert template.Template(
|
||||
"{{ flatten([[1, 'a'], [2, 'b']]) }}", hass
|
||||
).async_render() == [1, "a", 2, "b"]
|
||||
|
||||
# Test empty list
|
||||
assert template.Template("{{ flatten([]) }}", hass).async_render() == []
|
||||
|
||||
# Test single level
|
||||
assert template.Template("{{ flatten([1, 2, 3]) }}", hass).async_render() == [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
]
|
||||
|
||||
|
||||
def test_intersect(hass: HomeAssistant) -> None:
|
||||
"""Test intersect."""
|
||||
# Test basic intersection
|
||||
result = template.Template(
|
||||
"{{ [1, 2, 3, 4] | intersect([3, 4, 5, 6]) | sort }}", hass
|
||||
).async_render()
|
||||
assert result == [3, 4]
|
||||
|
||||
# Test no intersection
|
||||
result = template.Template("{{ [1, 2] | intersect([3, 4]) }}", hass).async_render()
|
||||
assert result == []
|
||||
|
||||
# Test string intersection
|
||||
result = template.Template(
|
||||
"{{ ['a', 'b', 'c'] | intersect(['b', 'c', 'd']) | sort }}", hass
|
||||
).async_render()
|
||||
assert result == ["b", "c"]
|
||||
|
||||
# Test empty list intersection
|
||||
result = template.Template("{{ [] | intersect([1, 2, 3]) }}", hass).async_render()
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_difference(hass: HomeAssistant) -> None:
|
||||
"""Test difference."""
|
||||
# Test basic difference
|
||||
result = template.Template(
|
||||
"{{ [1, 2, 3, 4] | difference([3, 4, 5, 6]) | sort }}", hass
|
||||
).async_render()
|
||||
assert result == [1, 2]
|
||||
|
||||
# Test no difference
|
||||
result = template.Template(
|
||||
"{{ [1, 2] | difference([1, 2, 3, 4]) }}", hass
|
||||
).async_render()
|
||||
assert result == []
|
||||
|
||||
# Test string difference
|
||||
result = template.Template(
|
||||
"{{ ['a', 'b', 'c'] | difference(['b', 'c', 'd']) | sort }}", hass
|
||||
).async_render()
|
||||
assert result == ["a"]
|
||||
|
||||
# Test empty list difference
|
||||
result = template.Template("{{ [] | difference([1, 2, 3]) }}", hass).async_render()
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_union(hass: HomeAssistant) -> None:
|
||||
"""Test union."""
|
||||
# Test basic union
|
||||
result = template.Template(
|
||||
"{{ [1, 2, 3] | union([3, 4, 5]) | sort }}", hass
|
||||
).async_render()
|
||||
assert result == [1, 2, 3, 4, 5]
|
||||
|
||||
# Test string union
|
||||
result = template.Template(
|
||||
"{{ ['a', 'b'] | union(['b', 'c']) | sort }}", hass
|
||||
).async_render()
|
||||
assert result == ["a", "b", "c"]
|
||||
|
||||
# Test empty list union
|
||||
result = template.Template(
|
||||
"{{ [] | union([1, 2, 3]) | sort }}", hass
|
||||
).async_render()
|
||||
assert result == [1, 2, 3]
|
||||
|
||||
# Test duplicate elements
|
||||
result = template.Template(
|
||||
"{{ [1, 1, 2, 2] | union([2, 2, 3, 3]) | sort }}", hass
|
||||
).async_render()
|
||||
assert result == [1, 2, 3]
|
||||
|
||||
|
||||
def test_symmetric_difference(hass: HomeAssistant) -> None:
|
||||
"""Test symmetric_difference."""
|
||||
# Test basic symmetric difference
|
||||
result = template.Template(
|
||||
"{{ [1, 2, 3, 4] | symmetric_difference([3, 4, 5, 6]) | sort }}", hass
|
||||
).async_render()
|
||||
assert result == [1, 2, 5, 6]
|
||||
|
||||
# Test no symmetric difference (identical sets)
|
||||
result = template.Template(
|
||||
"{{ [1, 2, 3] | symmetric_difference([1, 2, 3]) }}", hass
|
||||
).async_render()
|
||||
assert result == []
|
||||
|
||||
# Test string symmetric difference
|
||||
result = template.Template(
|
||||
"{{ ['a', 'b', 'c'] | symmetric_difference(['b', 'c', 'd']) | sort }}", hass
|
||||
).async_render()
|
||||
assert result == ["a", "d"]
|
||||
|
||||
# Test empty list symmetric difference
|
||||
result = template.Template(
|
||||
"{{ [] | symmetric_difference([1, 2, 3]) | sort }}", hass
|
||||
).async_render()
|
||||
assert result == [1, 2, 3]
|
||||
|
||||
|
||||
def test_collection_functions_as_tests(hass: HomeAssistant) -> None:
|
||||
"""Test that type checking functions work as tests."""
|
||||
# Test various type checking functions
|
||||
assert template.Template("{{ [1,2,3] is list }}", hass).async_render()
|
||||
assert template.Template("{{ set([1,2,3]) is set }}", hass).async_render()
|
||||
assert template.Template("{{ (1,2,3) is tuple }}", hass).async_render()
|
||||
|
||||
|
||||
def test_collection_error_handling(hass: HomeAssistant) -> None:
|
||||
"""Test error handling in collection functions."""
|
||||
|
||||
# Test flatten with non-iterable
|
||||
with pytest.raises(TemplateError, match="flatten expected a list"):
|
||||
template.Template("{{ flatten(123) }}", hass).async_render()
|
||||
|
||||
# Test intersect with non-iterable
|
||||
with pytest.raises(TemplateError, match="intersect expected a list"):
|
||||
template.Template("{{ [1, 2] | intersect(123) }}", hass).async_render()
|
||||
|
||||
# Test difference with non-iterable
|
||||
with pytest.raises(TemplateError, match="difference expected a list"):
|
||||
template.Template("{{ [1, 2] | difference(123) }}", hass).async_render()
|
||||
|
||||
# Test shuffle with no arguments
|
||||
with pytest.raises(TemplateError, match="shuffle expected at least 1 argument"):
|
||||
template.Template("{{ shuffle() }}", hass).async_render()
|
||||
@@ -15,7 +15,6 @@ from unittest.mock import patch
|
||||
from freezegun import freeze_time
|
||||
import orjson
|
||||
import pytest
|
||||
from pytest_unordered import unordered
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -514,114 +513,6 @@ def test_isnumber(hass: HomeAssistant, value, expected) -> None:
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
([1, 2], True),
|
||||
({1, 2}, False),
|
||||
({"a": 1, "b": 2}, False),
|
||||
(ReadOnlyDict({"a": 1, "b": 2}), False),
|
||||
(MappingProxyType({"a": 1, "b": 2}), False),
|
||||
("abc", False),
|
||||
(b"abc", False),
|
||||
((1, 2), False),
|
||||
(datetime(2024, 1, 1, 0, 0, 0), False),
|
||||
],
|
||||
)
|
||||
def test_is_list(hass: HomeAssistant, value: Any, expected: bool) -> None:
|
||||
"""Test is list."""
|
||||
assert (
|
||||
template.Template("{{ value is list }}", hass).async_render({"value": value})
|
||||
== expected
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
([1, 2], False),
|
||||
({1, 2}, True),
|
||||
({"a": 1, "b": 2}, False),
|
||||
(ReadOnlyDict({"a": 1, "b": 2}), False),
|
||||
(MappingProxyType({"a": 1, "b": 2}), False),
|
||||
("abc", False),
|
||||
(b"abc", False),
|
||||
((1, 2), False),
|
||||
(datetime(2024, 1, 1, 0, 0, 0), False),
|
||||
],
|
||||
)
|
||||
def test_is_set(hass: HomeAssistant, value: Any, expected: bool) -> None:
|
||||
"""Test is set."""
|
||||
assert (
|
||||
template.Template("{{ value is set }}", hass).async_render({"value": value})
|
||||
== expected
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
([1, 2], False),
|
||||
({1, 2}, False),
|
||||
({"a": 1, "b": 2}, False),
|
||||
(ReadOnlyDict({"a": 1, "b": 2}), False),
|
||||
(MappingProxyType({"a": 1, "b": 2}), False),
|
||||
("abc", False),
|
||||
(b"abc", False),
|
||||
((1, 2), True),
|
||||
(datetime(2024, 1, 1, 0, 0, 0), False),
|
||||
],
|
||||
)
|
||||
def test_is_tuple(hass: HomeAssistant, value: Any, expected: bool) -> None:
|
||||
"""Test is tuple."""
|
||||
assert (
|
||||
template.Template("{{ value is tuple }}", hass).async_render({"value": value})
|
||||
== expected
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
([1, 2], {1, 2}),
|
||||
({1, 2}, {1, 2}),
|
||||
({"a": 1, "b": 2}, {"a", "b"}),
|
||||
(ReadOnlyDict({"a": 1, "b": 2}), {"a", "b"}),
|
||||
(MappingProxyType({"a": 1, "b": 2}), {"a", "b"}),
|
||||
("abc", {"a", "b", "c"}),
|
||||
(b"abc", {97, 98, 99}),
|
||||
((1, 2), {1, 2}),
|
||||
],
|
||||
)
|
||||
def test_set(hass: HomeAssistant, value: Any, expected: bool) -> None:
|
||||
"""Test convert to set function."""
|
||||
assert (
|
||||
template.Template("{{ set(value) }}", hass).async_render({"value": value})
|
||||
== expected
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
([1, 2], (1, 2)),
|
||||
({1, 2}, (1, 2)),
|
||||
({"a": 1, "b": 2}, ("a", "b")),
|
||||
(ReadOnlyDict({"a": 1, "b": 2}), ("a", "b")),
|
||||
(MappingProxyType({"a": 1, "b": 2}), ("a", "b")),
|
||||
("abc", ("a", "b", "c")),
|
||||
(b"abc", (97, 98, 99)),
|
||||
((1, 2), (1, 2)),
|
||||
],
|
||||
)
|
||||
def test_tuple(hass: HomeAssistant, value: Any, expected: bool) -> None:
|
||||
"""Test convert to tuple function."""
|
||||
assert (
|
||||
template.Template("{{ tuple(value) }}", hass).async_render({"value": value})
|
||||
== expected
|
||||
)
|
||||
|
||||
|
||||
def test_converting_datetime_to_iterable(hass: HomeAssistant) -> None:
|
||||
"""Test converting a datetime to an iterable raises an error."""
|
||||
dt_ = datetime(2020, 1, 1, 0, 0, 0)
|
||||
@@ -655,30 +546,6 @@ def test_is_datetime(hass: HomeAssistant, value, expected) -> None:
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
([1, 2], False),
|
||||
({1, 2}, False),
|
||||
({"a": 1, "b": 2}, False),
|
||||
(ReadOnlyDict({"a": 1, "b": 2}), False),
|
||||
(MappingProxyType({"a": 1, "b": 2}), False),
|
||||
("abc", True),
|
||||
(b"abc", True),
|
||||
((1, 2), False),
|
||||
(datetime(2024, 1, 1, 0, 0, 0), False),
|
||||
],
|
||||
)
|
||||
def test_is_string_like(hass: HomeAssistant, value, expected) -> None:
|
||||
"""Test is string_like."""
|
||||
assert (
|
||||
template.Template("{{ value is string_like }}", hass).async_render(
|
||||
{"value": value}
|
||||
)
|
||||
== expected
|
||||
)
|
||||
|
||||
|
||||
def test_rounding_value(hass: HomeAssistant) -> None:
|
||||
"""Test rounding value."""
|
||||
hass.states.async_set("sensor.temperature", 12.78)
|
||||
@@ -795,37 +662,46 @@ def test_apply(hass: HomeAssistant) -> None:
|
||||
def test_apply_macro_with_arguments(hass: HomeAssistant) -> None:
|
||||
"""Test apply macro with positional, named, and mixed arguments."""
|
||||
# Test macro with positional arguments
|
||||
assert template.Template(
|
||||
"""
|
||||
{%- macro greet(name, greeting) -%}
|
||||
{{ greeting }}, {{ name }}!
|
||||
{%- endmacro %}
|
||||
{{ ["Alice", "Bob"] | map('apply', greet, "Hello") | list }}
|
||||
assert (
|
||||
template.Template(
|
||||
"""
|
||||
{%- macro add_numbers(a, b, c) -%}
|
||||
{{ a + b + c }}
|
||||
{%- endmacro -%}
|
||||
{{ apply(5, add_numbers, 10, 15) }}
|
||||
""",
|
||||
hass,
|
||||
).async_render() == ["Hello, Alice!", "Hello, Bob!"]
|
||||
hass,
|
||||
).async_render()
|
||||
== 30
|
||||
)
|
||||
|
||||
# Test macro with named arguments
|
||||
assert template.Template(
|
||||
"""
|
||||
{%- macro greet(name, greeting="Hi") -%}
|
||||
assert (
|
||||
template.Template(
|
||||
"""
|
||||
{%- macro greet(name, greeting="Hello") -%}
|
||||
{{ greeting }}, {{ name }}!
|
||||
{%- endmacro %}
|
||||
{{ ["Alice", "Bob"] | map('apply', greet, greeting="Hello") | list }}
|
||||
{%- endmacro -%}
|
||||
{{ apply("World", greet, greeting="Hi") }}
|
||||
""",
|
||||
hass,
|
||||
).async_render() == ["Hello, Alice!", "Hello, Bob!"]
|
||||
hass,
|
||||
).async_render()
|
||||
== "Hi, World!"
|
||||
)
|
||||
|
||||
# Test macro with mixed positional and named arguments
|
||||
assert template.Template(
|
||||
"""
|
||||
{%- macro greet(name, separator, greeting="Hi") -%}
|
||||
{{ greeting }}{{separator}} {{ name }}!
|
||||
{%- endmacro %}
|
||||
{{ ["Alice", "Bob"] | map('apply', greet, "," , greeting="Hey") | list }}
|
||||
# Test macro with mixed arguments
|
||||
assert (
|
||||
template.Template(
|
||||
"""
|
||||
{%- macro format_message(prefix, name, suffix="!") -%}
|
||||
{{ prefix }} {{ name }}{{ suffix }}
|
||||
{%- endmacro -%}
|
||||
{{ apply("Welcome", format_message, "John", suffix="...") }}
|
||||
""",
|
||||
hass,
|
||||
).async_render() == ["Hey, Alice!", "Hey, Bob!"]
|
||||
hass,
|
||||
).async_render()
|
||||
== "Welcome John..."
|
||||
)
|
||||
|
||||
|
||||
def test_as_function(hass: HomeAssistant) -> None:
|
||||
@@ -5695,51 +5571,6 @@ async def test_template_thread_safety_checks(hass: HomeAssistant) -> None:
|
||||
assert template_obj.async_render_to_info().result() == 23
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("cola", "colb", "expected"),
|
||||
[
|
||||
([1, 2], [3, 4], [(1, 3), (2, 4)]),
|
||||
([1, 2], [3, 4, 5], [(1, 3), (2, 4)]),
|
||||
([1, 2, 3, 4], [3, 4], [(1, 3), (2, 4)]),
|
||||
],
|
||||
)
|
||||
def test_zip(hass: HomeAssistant, cola, colb, expected) -> None:
|
||||
"""Test zip."""
|
||||
assert (
|
||||
template.Template("{{ zip(cola, colb) | list }}", hass).async_render(
|
||||
{"cola": cola, "colb": colb}
|
||||
)
|
||||
== expected
|
||||
)
|
||||
assert (
|
||||
template.Template(
|
||||
"[{% for a, b in zip(cola, colb) %}({{a}}, {{b}}), {% endfor %}]", hass
|
||||
).async_render({"cola": cola, "colb": colb})
|
||||
== expected
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("col", "expected"),
|
||||
[
|
||||
([(1, 3), (2, 4)], [(1, 2), (3, 4)]),
|
||||
(["ax", "by", "cz"], [("a", "b", "c"), ("x", "y", "z")]),
|
||||
],
|
||||
)
|
||||
def test_unzip(hass: HomeAssistant, col, expected) -> None:
|
||||
"""Test unzipping using zip."""
|
||||
assert (
|
||||
template.Template("{{ zip(*col) | list }}", hass).async_render({"col": col})
|
||||
== expected
|
||||
)
|
||||
assert (
|
||||
template.Template(
|
||||
"{% set a, b = zip(*col) %}[{{a}}, {{b}}]", hass
|
||||
).async_render({"col": col})
|
||||
== expected
|
||||
)
|
||||
|
||||
|
||||
def test_template_output_exceeds_maximum_size(hass: HomeAssistant) -> None:
|
||||
"""Test template output exceeds maximum size."""
|
||||
tpl = template.Template("{{ 'a' * 1024 * 257 }}", hass)
|
||||
@@ -6040,57 +5871,6 @@ async def test_merge_response_not_mutate_original_object(
|
||||
assert tpl.async_render()
|
||||
|
||||
|
||||
def test_shuffle(hass: HomeAssistant) -> None:
|
||||
"""Test the shuffle function and filter."""
|
||||
assert list(
|
||||
template.Template("{{ [1, 2, 3] | shuffle }}", hass).async_render()
|
||||
) == unordered([1, 2, 3])
|
||||
|
||||
assert list(
|
||||
template.Template("{{ shuffle([1, 2, 3]) }}", hass).async_render()
|
||||
) == unordered([1, 2, 3])
|
||||
|
||||
assert list(
|
||||
template.Template("{{ shuffle(1, 2, 3) }}", hass).async_render()
|
||||
) == unordered([1, 2, 3])
|
||||
|
||||
assert list(template.Template("{{ shuffle([]) }}", hass).async_render()) == []
|
||||
|
||||
assert list(template.Template("{{ [] | shuffle }}", hass).async_render()) == []
|
||||
|
||||
# Testing using seed
|
||||
assert list(
|
||||
template.Template("{{ shuffle([1, 2, 3], 'seed') }}", hass).async_render()
|
||||
) == [2, 3, 1]
|
||||
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ shuffle([1, 2, 3], seed='seed') }}",
|
||||
hass,
|
||||
).async_render()
|
||||
) == [2, 3, 1]
|
||||
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ [1, 2, 3] | shuffle('seed') }}",
|
||||
hass,
|
||||
).async_render()
|
||||
) == [2, 3, 1]
|
||||
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ [1, 2, 3] | shuffle(seed='seed') }}",
|
||||
hass,
|
||||
).async_render()
|
||||
) == [2, 3, 1]
|
||||
|
||||
with pytest.raises(TemplateError):
|
||||
template.Template("{{ 1 | shuffle }}", hass).async_render()
|
||||
|
||||
with pytest.raises(TemplateError):
|
||||
template.Template("{{ shuffle() }}", hass).async_render()
|
||||
|
||||
|
||||
def test_typeof(hass: HomeAssistant) -> None:
|
||||
"""Test the typeof debug filter/function."""
|
||||
assert template.Template("{{ True | typeof }}", hass).async_render() == "bool"
|
||||
@@ -6118,221 +5898,6 @@ def test_typeof(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_flatten(hass: HomeAssistant) -> None:
|
||||
"""Test the flatten function and filter."""
|
||||
assert template.Template(
|
||||
"{{ flatten([1, [2, [3]], 4, [5 , 6]]) }}", hass
|
||||
).async_render() == [1, 2, 3, 4, 5, 6]
|
||||
|
||||
assert template.Template(
|
||||
"{{ [1, [2, [3]], 4, [5 , 6]] | flatten }}", hass
|
||||
).async_render() == [1, 2, 3, 4, 5, 6]
|
||||
|
||||
assert template.Template(
|
||||
"{{ flatten([1, [2, [3]], 4, [5 , 6]], 1) }}", hass
|
||||
).async_render() == [1, 2, [3], 4, 5, 6]
|
||||
|
||||
assert template.Template(
|
||||
"{{ flatten([1, [2, [3]], 4, [5 , 6]], levels=1) }}", hass
|
||||
).async_render() == [1, 2, [3], 4, 5, 6]
|
||||
|
||||
assert template.Template(
|
||||
"{{ [1, [2, [3]], 4, [5 , 6]] | flatten(1) }}", hass
|
||||
).async_render() == [1, 2, [3], 4, 5, 6]
|
||||
|
||||
assert template.Template(
|
||||
"{{ [1, [2, [3]], 4, [5 , 6]] | flatten(levels=1) }}", hass
|
||||
).async_render() == [1, 2, [3], 4, 5, 6]
|
||||
|
||||
assert template.Template("{{ flatten([]) }}", hass).async_render() == []
|
||||
|
||||
assert template.Template("{{ [] | flatten }}", hass).async_render() == []
|
||||
|
||||
with pytest.raises(TemplateError):
|
||||
template.Template("{{ 'string' | flatten }}", hass).async_render()
|
||||
|
||||
with pytest.raises(TemplateError):
|
||||
template.Template("{{ flatten() }}", hass).async_render()
|
||||
|
||||
|
||||
def test_intersect(hass: HomeAssistant) -> None:
|
||||
"""Test the intersect function and filter."""
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ intersect([1, 2, 5, 3, 4, 10], [1, 2, 3, 4, 5, 11, 99]) }}", hass
|
||||
).async_render()
|
||||
) == unordered([1, 2, 3, 4, 5])
|
||||
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ [1, 2, 5, 3, 4, 10] | intersect([1, 2, 3, 4, 5, 11, 99]) }}", hass
|
||||
).async_render()
|
||||
) == unordered([1, 2, 3, 4, 5])
|
||||
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ intersect(['a', 'b', 'c'], ['b', 'c', 'd']) }}", hass
|
||||
).async_render()
|
||||
) == unordered(["b", "c"])
|
||||
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ ['a', 'b', 'c'] | intersect(['b', 'c', 'd']) }}", hass
|
||||
).async_render()
|
||||
) == unordered(["b", "c"])
|
||||
|
||||
assert (
|
||||
template.Template("{{ intersect([], [1, 2, 3]) }}", hass).async_render() == []
|
||||
)
|
||||
|
||||
assert (
|
||||
template.Template("{{ [] | intersect([1, 2, 3]) }}", hass).async_render() == []
|
||||
)
|
||||
|
||||
with pytest.raises(TemplateError, match="intersect expected a list, got str"):
|
||||
template.Template("{{ 'string' | intersect([1, 2, 3]) }}", hass).async_render()
|
||||
|
||||
with pytest.raises(TemplateError, match="intersect expected a list, got str"):
|
||||
template.Template("{{ [1, 2, 3] | intersect('string') }}", hass).async_render()
|
||||
|
||||
|
||||
def test_difference(hass: HomeAssistant) -> None:
|
||||
"""Test the difference function and filter."""
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ difference([1, 2, 5, 3, 4, 10], [1, 2, 3, 4, 5, 11, 99]) }}", hass
|
||||
).async_render()
|
||||
) == [10]
|
||||
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ [1, 2, 5, 3, 4, 10] | difference([1, 2, 3, 4, 5, 11, 99]) }}", hass
|
||||
).async_render()
|
||||
) == [10]
|
||||
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ difference(['a', 'b', 'c'], ['b', 'c', 'd']) }}", hass
|
||||
).async_render()
|
||||
) == ["a"]
|
||||
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ ['a', 'b', 'c'] | difference(['b', 'c', 'd']) }}", hass
|
||||
).async_render()
|
||||
) == ["a"]
|
||||
|
||||
assert (
|
||||
template.Template("{{ difference([], [1, 2, 3]) }}", hass).async_render() == []
|
||||
)
|
||||
|
||||
assert (
|
||||
template.Template("{{ [] | difference([1, 2, 3]) }}", hass).async_render() == []
|
||||
)
|
||||
|
||||
with pytest.raises(TemplateError, match="difference expected a list, got str"):
|
||||
template.Template("{{ 'string' | difference([1, 2, 3]) }}", hass).async_render()
|
||||
|
||||
with pytest.raises(TemplateError, match="difference expected a list, got str"):
|
||||
template.Template("{{ [1, 2, 3] | difference('string') }}", hass).async_render()
|
||||
|
||||
|
||||
def test_union(hass: HomeAssistant) -> None:
|
||||
"""Test the union function and filter."""
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ union([1, 2, 5, 3, 4, 10], [1, 2, 3, 4, 5, 11, 99]) }}", hass
|
||||
).async_render()
|
||||
) == unordered([1, 2, 3, 4, 5, 10, 11, 99])
|
||||
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ [1, 2, 5, 3, 4, 10] | union([1, 2, 3, 4, 5, 11, 99]) }}", hass
|
||||
).async_render()
|
||||
) == unordered([1, 2, 3, 4, 5, 10, 11, 99])
|
||||
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ union(['a', 'b', 'c'], ['b', 'c', 'd']) }}", hass
|
||||
).async_render()
|
||||
) == unordered(["a", "b", "c", "d"])
|
||||
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ ['a', 'b', 'c'] | union(['b', 'c', 'd']) }}", hass
|
||||
).async_render()
|
||||
) == unordered(["a", "b", "c", "d"])
|
||||
|
||||
assert list(
|
||||
template.Template("{{ union([], [1, 2, 3]) }}", hass).async_render()
|
||||
) == unordered([1, 2, 3])
|
||||
|
||||
assert list(
|
||||
template.Template("{{ [] | union([1, 2, 3]) }}", hass).async_render()
|
||||
) == unordered([1, 2, 3])
|
||||
|
||||
with pytest.raises(TemplateError, match="union expected a list, got str"):
|
||||
template.Template("{{ 'string' | union([1, 2, 3]) }}", hass).async_render()
|
||||
|
||||
with pytest.raises(TemplateError, match="union expected a list, got str"):
|
||||
template.Template("{{ [1, 2, 3] | union('string') }}", hass).async_render()
|
||||
|
||||
|
||||
def test_symmetric_difference(hass: HomeAssistant) -> None:
|
||||
"""Test the symmetric_difference function and filter."""
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ symmetric_difference([1, 2, 5, 3, 4, 10], [1, 2, 3, 4, 5, 11, 99]) }}",
|
||||
hass,
|
||||
).async_render()
|
||||
) == unordered([10, 11, 99])
|
||||
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ [1, 2, 5, 3, 4, 10] | symmetric_difference([1, 2, 3, 4, 5, 11, 99]) }}",
|
||||
hass,
|
||||
).async_render()
|
||||
) == unordered([10, 11, 99])
|
||||
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ symmetric_difference(['a', 'b', 'c'], ['b', 'c', 'd']) }}", hass
|
||||
).async_render()
|
||||
) == unordered(["a", "d"])
|
||||
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ ['a', 'b', 'c'] | symmetric_difference(['b', 'c', 'd']) }}", hass
|
||||
).async_render()
|
||||
) == unordered(["a", "d"])
|
||||
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ symmetric_difference([], [1, 2, 3]) }}", hass
|
||||
).async_render()
|
||||
) == unordered([1, 2, 3])
|
||||
|
||||
assert list(
|
||||
template.Template(
|
||||
"{{ [] | symmetric_difference([1, 2, 3]) }}", hass
|
||||
).async_render()
|
||||
) == unordered([1, 2, 3])
|
||||
|
||||
with pytest.raises(
|
||||
TemplateError, match="symmetric_difference expected a list, got str"
|
||||
):
|
||||
template.Template(
|
||||
"{{ 'string' | symmetric_difference([1, 2, 3]) }}", hass
|
||||
).async_render()
|
||||
|
||||
with pytest.raises(
|
||||
TemplateError, match="symmetric_difference expected a list, got str"
|
||||
):
|
||||
template.Template(
|
||||
"{{ [1, 2, 3] | symmetric_difference('string') }}", hass
|
||||
).async_render()
|
||||
|
||||
|
||||
def test_combine(hass: HomeAssistant) -> None:
|
||||
"""Test combine filter and function."""
|
||||
assert template.Template(
|
||||
|
||||
Reference in New Issue
Block a user