Use built-in Jinja min and max filters in templates (#60327)

* Use built-in Jinja min and max filters in templates

* use built-in filter for global

* lint

* less generic name

* more tests

* even more tests
This commit is contained in:
avee87 2022-01-04 09:07:23 +00:00 committed by GitHub
parent 2d0aaeba6b
commit 04606f05a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 112 additions and 5 deletions

View File

@ -24,7 +24,7 @@ from urllib.parse import urlencode as urllib_urlencode
import weakref import weakref
import jinja2 import jinja2
from jinja2 import pass_context from jinja2 import pass_context, pass_environment
from jinja2.sandbox import ImmutableSandboxedEnvironment from jinja2.sandbox import ImmutableSandboxedEnvironment
from jinja2.utils import Namespace from jinja2.utils import Namespace
import voluptuous as vol import voluptuous as vol
@ -1525,6 +1525,30 @@ def fail_when_undefined(value):
return 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: def average(*args: Any) -> float:
""" """
Filter and function to calculate the arithmetic mean of an iterable or of two or more arguments. 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["from_json"] = from_json
self.filters["is_defined"] = fail_when_undefined self.filters["is_defined"] = fail_when_undefined
self.filters["average"] = average self.filters["average"] = average
self.filters["max"] = max
self.filters["min"] = min
self.filters["random"] = random_every_time self.filters["random"] = random_every_time
self.filters["base64_encode"] = base64_encode self.filters["base64_encode"] = base64_encode
self.filters["base64_decode"] = base64_decode self.filters["base64_decode"] = base64_decode
@ -1909,8 +1931,8 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.globals["strptime"] = strptime self.globals["strptime"] = strptime
self.globals["urlencode"] = urlencode self.globals["urlencode"] = urlencode
self.globals["average"] = average self.globals["average"] = average
self.globals["max"] = max self.globals["max"] = min_max_from_filter(self.filters["max"], "max")
self.globals["min"] = min self.globals["min"] = min_max_from_filter(self.filters["min"], "min")
self.globals["is_number"] = is_number self.globals["is_number"] = is_number
self.globals["int"] = forgiving_int self.globals["int"] = forgiving_int
self.globals["pack"] = struct_pack self.globals["pack"] = struct_pack

View File

@ -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
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): def test_max(hass):
"""Test the max filter.""" """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
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): def test_ord(hass):
"""Test the ord filter.""" """Test the ord filter."""