diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index ce7984c03bb..150d049b1b8 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -24,7 +24,7 @@ from urllib.parse import urlencode as urllib_urlencode import weakref import jinja2 -from jinja2 import pass_context +from jinja2 import pass_context, pass_environment from jinja2.sandbox import ImmutableSandboxedEnvironment from jinja2.utils import Namespace import voluptuous as vol @@ -1525,6 +1525,30 @@ def fail_when_undefined(value): return value +def min_max_from_filter(builtin_filter: Any, name: str) -> Any: + """ + Convert a built-in min/max Jinja filter to a global function. + + The parameters may be passed as an iterable or as separate arguments. + """ + + @pass_environment + @wraps(builtin_filter) + def wrapper(environment: jinja2.Environment, *args: Any, **kwargs: Any) -> Any: + if len(args) == 0: + raise TypeError(f"{name} expected at least 1 argument, got 0") + + if len(args) == 1: + if isinstance(args[0], Iterable): + return builtin_filter(environment, args[0], **kwargs) + + raise TypeError(f"'{type(args[0]).__name__}' object is not iterable") + + return builtin_filter(environment, args, **kwargs) + + return pass_environment(wrapper) + + def average(*args: Any) -> float: """ Filter and function to calculate the arithmetic mean of an iterable or of two or more arguments. @@ -1865,8 +1889,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["from_json"] = from_json self.filters["is_defined"] = fail_when_undefined self.filters["average"] = average - self.filters["max"] = max - self.filters["min"] = min self.filters["random"] = random_every_time self.filters["base64_encode"] = base64_encode self.filters["base64_decode"] = base64_decode @@ -1909,8 +1931,8 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["strptime"] = strptime self.globals["urlencode"] = urlencode self.globals["average"] = average - self.globals["max"] = max - self.globals["min"] = min + self.globals["max"] = min_max_from_filter(self.filters["max"], "max") + self.globals["min"] = min_max_from_filter(self.filters["min"], "min") self.globals["is_number"] = is_number self.globals["int"] = forgiving_int self.globals["pack"] = struct_pack diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 471a5b2c486..dd6d29a8c95 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -835,6 +835,15 @@ def test_min(hass): assert template.Template("{{ min([1, 2, 3]) }}", hass).async_render() == 1 assert template.Template("{{ min(1, 2, 3) }}", hass).async_render() == 1 + with pytest.raises(TemplateError): + template.Template("{{ 1 | min }}", hass).async_render() + + with pytest.raises(TemplateError): + template.Template("{{ min() }}", hass).async_render() + + with pytest.raises(TemplateError): + template.Template("{{ min(1) }}", hass).async_render() + def test_max(hass): """Test the max filter.""" @@ -842,6 +851,82 @@ def test_max(hass): assert template.Template("{{ max([1, 2, 3]) }}", hass).async_render() == 3 assert template.Template("{{ max(1, 2, 3) }}", hass).async_render() == 3 + with pytest.raises(TemplateError): + template.Template("{{ 1 | max }}", hass).async_render() + + with pytest.raises(TemplateError): + template.Template("{{ max() }}", hass).async_render() + + with pytest.raises(TemplateError): + template.Template("{{ max(1) }}", hass).async_render() + + +@pytest.mark.parametrize( + "attribute", + ( + "a", + "b", + "c", + ), +) +def test_min_max_attribute(hass, attribute): + """Test the min and max filters with attribute.""" + hass.states.async_set( + "test.object", + "test", + { + "objects": [ + { + "a": 1, + "b": 2, + "c": 3, + }, + { + "a": 2, + "b": 1, + "c": 2, + }, + { + "a": 3, + "b": 3, + "c": 1, + }, + ], + }, + ) + assert ( + template.Template( + "{{ (state_attr('test.object', 'objects') | min(attribute='%s'))['%s']}}" + % (attribute, attribute), + hass, + ).async_render() + == 1 + ) + assert ( + template.Template( + "{{ (min(state_attr('test.object', 'objects'), attribute='%s'))['%s']}}" + % (attribute, attribute), + hass, + ).async_render() + == 1 + ) + assert ( + template.Template( + "{{ (state_attr('test.object', 'objects') | max(attribute='%s'))['%s']}}" + % (attribute, attribute), + hass, + ).async_render() + == 3 + ) + assert ( + template.Template( + "{{ (max(state_attr('test.object', 'objects'), attribute='%s'))['%s']}}" + % (attribute, attribute), + hass, + ).async_render() + == 3 + ) + def test_ord(hass): """Test the ord filter."""