diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 2722567547d..9ac6286443e 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -477,7 +477,7 @@ ApplyPublish = Literal["before", "after"] | None class ApplyEntity: """A domain's ``Apply*`` class, its generated function's parameters and its publish statement. - ``locals`` are (name, declaration) pairs, emitted only when the body uses the name. + ``locals`` are (name, declaration) pairs, declared only when the body uses the name. """ cls: MockObjClass @@ -567,12 +567,11 @@ class SubEntities: body.insert(0, apply_entity.publish) elif publish == "after": body.append(apply_entity.publish) - text = "\n".join(body) - body[:0] = [ - decl - for name, decl in apply_entity.locals - if re.search(rf"\b{name}\b", text) - ] + for name, decl in apply_entity.locals: + # Declared right before its first use, so calls keep their order. + uses = (i for i, line in enumerate(body) if re.search(rf"\b{name}\b", line)) + if (first := next(uses, None)) is not None: + body.insert(first, decl) functions: dict[str, SafeExpType] = CORE.data.setdefault(_APPLY_DOMAIN, {}) key = "\n".join([*(f"{t} {n}" for t, n in apply_entity.params), *body]) if (fn := functions.get(key)) is None: diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index 4d42e12e541..e4f2ec0b8e4 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -1396,17 +1396,28 @@ _SELECT_APPLY = entity_helpers.ApplyEntity( @pytest.mark.asyncio @pytest.mark.parametrize( - ("apply", "body"), + ("apply", "publish", "body"), [ ( "set_mode(option)", + "after", ( "const char *option = entity->option_at(index);\n" "::hub->set_mode(option);\nentity->publish_state(index);" ), ), + ( + "set_mode(option)", + "before", + ( + "entity->publish_state(index);\n" + "const char *option = entity->option_at(index);\n" + "::hub->set_mode(option);" + ), + ), ( "set_mode(index, options_)", + "after", "::hub->set_mode(index, options_);\nentity->publish_state(index);", ), ], @@ -1415,12 +1426,13 @@ async def test_sub_entities_apply_locals( setup_test_environment: list[str], apply_functions: list[tuple[str, str, str]], apply: str, + publish: str, body: str, ) -> None: - """A local is declared only when the body names it as a whole identifier.""" + """A local is declared before its first use, and only when named as a whole identifier.""" conf = _apply_conf("m_sel", _SELECT_APPLY.cls) sub = _apply_sub([], {"m": conf}, _SELECT_APPLY) - await sub("m", None, apply=apply, publish="after") + await sub("m", None, apply=apply, publish=publish) assert apply_functions == [ ("esphome__m_sel__fn", "select::Select * entity, size_t index", body) ]