Add template function: shuffle (#140077)

This commit is contained in:
Franck Nijhof 2025-03-08 02:36:17 +01:00 committed by GitHub
parent b7094c12f7
commit d4f205c366
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 79 additions and 1 deletions

View File

@ -6,7 +6,7 @@ from ast import literal_eval
import asyncio
import base64
import collections.abc
from collections.abc import Callable, Generator, Iterable
from collections.abc import Callable, Generator, Iterable, MutableSequence
from contextlib import AbstractContextManager
from contextvars import ContextVar
from copy import deepcopy
@ -2736,6 +2736,30 @@ 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
class TemplateContextManager(AbstractContextManager):
"""Context manager to store template being parsed or rendered in a ContextVar."""
@ -2936,6 +2960,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.filters["bool"] = forgiving_boolean
self.filters["version"] = version
self.filters["contains"] = contains
self.filters["shuffle"] = shuffle
self.globals["log"] = logarithm
self.globals["sin"] = sine
self.globals["cos"] = cosine
@ -2973,6 +2998,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.globals["bool"] = forgiving_boolean
self.globals["version"] = version
self.globals["zip"] = zip
self.globals["shuffle"] = shuffle
self.tests["is_number"] = is_number
self.tests["list"] = _is_list
self.tests["set"] = _is_set

View File

@ -15,6 +15,7 @@ from unittest.mock import patch
from freezegun import freeze_time
import orjson
import pytest
from pytest_unordered import unordered
from syrupy import SnapshotAssertion
import voluptuous as vol
@ -6672,3 +6673,54 @@ async def test_merge_response_not_mutate_original_object(
tpl = template.Template(_template, hass)
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()