[core] Let a nested repeat shadow the outer iteration variable (#20634)

This commit is contained in:
Jonathan Swoboda
2026-10-13 14:35:24 -04:00
committed by GitHub
parent a827058928
commit 0821c84b7a
3 changed files with 97 additions and 2 deletions
+16 -1
View File
@@ -906,6 +906,21 @@ async def while_action_to_code(
return var
def _shadow_iteration(args: TemplateArgsType) -> TemplateArgsType:
"""Rename an inherited `iteration` so the inner repeat's own counter can shadow it."""
names = {name for _, name in args}
renamed: TemplateArgsType = []
for type_, name in args:
if name == "iteration":
depth = 1
while f"iteration_{depth}" in names:
depth += 1
name = f"iteration_{depth}"
names.add(name)
renamed.append((type_, name))
return renamed
@register_action(
"repeat",
RepeatAction,
@@ -929,7 +944,7 @@ async def repeat_action_to_code(
actions = await build_action_list(
config[CONF_THEN],
cg.TemplateArguments(cg.uint32, *template_arg.args),
[(cg.uint32, "iteration"), *args],
[(cg.uint32, "iteration"), *_shadow_iteration(args)],
)
for action in actions:
cg.add(var.add_then_action(action))
+17
View File
@@ -23,6 +23,10 @@ esphome:
then:
- logger.log: my_script is running
globals:
- id: nested_total
type: int
script:
- id: my_script
mode: single
@@ -71,3 +75,16 @@ script:
count: 5
then:
- logger.log: looping!
- repeat:
count: 2
then:
- repeat:
count: 2
then:
- delay: 200ms
- lambda: 'ESP_LOGD("main", "inner iteration %u", iteration);'
- globals.set:
id: nested_total
value: !lambda |-
int total = iteration;
return total;
+64 -1
View File
@@ -13,9 +13,13 @@ from esphome.automation import (
ApplyCondition,
ApplyField,
CallbackAutomation,
LambdaAction,
RepeatAction,
TriggerForwarder,
TriggerOnFalseForwarder,
TriggerOnTrueForwarder,
_shadow_iteration,
build_action,
build_callback_automation,
build_callback_automations,
build_parent_callback_automation,
@@ -41,7 +45,13 @@ from esphome.automation import (
)
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_AUTOMATION_ID, CONF_ID, CONF_THEN
from esphome.const import (
CONF_AUTOMATION_ID,
CONF_COUNT,
CONF_ID,
CONF_THEN,
CONF_TYPE_ID,
)
from esphome.core import CORE, ID, KEY_CORE, KEY_TARGET_PLATFORM, EsphomeError, Lambda
from esphome.cpp_generator import MockObj, RawExpression
from esphome.types import ConfigType
@@ -1545,3 +1555,56 @@ async def test_build_callback_automation_forwarder(mock_cg: MockCodegen) -> None
assert str(mock_cg.add.call_args.args[0]) == (
f"parent->add_cb(TriggerForwarder<bool>{{{NEW_OBJ}}})"
)
def test_shadow_iteration_leaves_other_names() -> None:
args = [(cg.std_string, "prefix"), (cg.uint32, "iteration_1")]
assert _shadow_iteration(args) == args
def test_shadow_iteration_renames_inherited_counter() -> None:
assert _shadow_iteration([(cg.uint32, "iteration"), (cg.bool_, "x")]) == [
(cg.uint32, "iteration_1"),
(cg.bool_, "x"),
]
def test_shadow_iteration_picks_first_unused_suffix() -> None:
"""A counter keeps its suffix at deeper levels; the new one takes the next free name."""
args = [
(cg.uint32, "iteration"),
(cg.uint32, "iteration_1"),
(cg.int_, "iteration_3"),
]
assert _shadow_iteration(args) == [
(cg.uint32, "iteration_2"),
(cg.uint32, "iteration_1"),
(cg.int_, "iteration_3"),
]
def _repeat_conf(id_: str, then: list[ConfigType]) -> ConfigType:
return {
"repeat": {CONF_COUNT: 2, CONF_THEN: then},
CONF_TYPE_ID: ID(id_, is_declaration=True, type=RepeatAction),
}
@pytest.mark.asyncio
async def test_nested_repeat_lambda_parameters_are_distinct() -> None:
"""Each repeat level adds its own `iteration`; inherited ones are renamed, not duplicated."""
CORE.config = {}
log = {
"lambda": Lambda('ESP_LOGD("t", "%u", iteration);'),
CONF_TYPE_ID: ID("log_action", is_declaration=True, type=LambdaAction),
}
conf = _repeat_conf(
"outer", [_repeat_conf("middle", [_repeat_conf("inner", [log])])]
)
await build_action(conf, cg.TemplateArguments(cg.uint8), [(cg.uint8, "level")])
text = CORE.cpp_main_section
assert (
"(uint32_t iteration, uint32_t iteration_2, uint32_t iteration_1, uint8_t level)"
in text
)
assert "uint32_t iteration, uint32_t iteration)" not in text