diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index c069409360..fe83b1ea7c 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -9,7 +9,6 @@ from esphome.const import ( CONF_VALUE, ) from esphome.core import CoroPriority, coroutine_with_priority -from esphome.cpp_generator import LambdaExpression from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] @@ -109,16 +108,8 @@ async def globals_set_to_code(config, action_id, template_arg, args): full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID]) template_arg = cg.TemplateArguments(full_id.type, *template_arg) var = cg.new_Pvariable(action_id, template_arg, paren) - value = config[CONF_VALUE] - if cg.is_template(value): - templ = await cg.templatable(value, args, None, to_exp=cg.RawExpression) - else: - # Wrap raw constant in a stateless lambda for TemplatableFn storage. - # Use RawExpression for the value since T is a template parameter - # (the C++ compiler handles the type deduction). - raw_value = cg.RawExpression(value) - templ = LambdaExpression( - f"return {cg.safe_exp(raw_value)};", args, capture="", return_type=None - ) + templ = await cg.templatable( + config[CONF_VALUE], args, None, to_exp=cg.RawExpression + ) cg.add(var.set_value(templ)) return var diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index c41171257b..814e37e02c 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -848,9 +848,11 @@ async def templatable( # Automatically wrap static strings in ESPHOME_F() for PROGMEM storage on ESP8266. # On other platforms ESPHOME_F() is a no-op returning const char*. return FlashStringLiteral(value) - # For non-string types, wrap constants in stateless lambdas so that - # TemplatableFn (used by TEMPLATABLE_VALUE macro) stores them as function pointers. - if output_type is not None and output_type is not std_string: + # Wrap non-string constants in stateless lambdas so that TemplatableFn + # (used by TEMPLATABLE_VALUE macro) stores them as function pointers. + # When output_type is None, the lambda omits the return type annotation + # and the C++ compiler deduces it (used by globals where T is unknown). + if output_type is not std_string: return LambdaExpression( f"return {safe_exp(value)};", args, diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 81ae586e23..c75851df0c 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -652,11 +652,11 @@ async def test_templatable__empty_string_with_std_string() -> None: @pytest.mark.asyncio async def test_templatable__string_with_none_output_type() -> None: - """Static string with output_type=None returns raw string (no wrapping).""" + """Static string with output_type=None returns stateless lambda (no return type).""" result = await cg.templatable("hello", [], None) - assert isinstance(result, str) - assert result == "hello" + assert isinstance(result, cg.LambdaExpression) + assert result.capture == "" @pytest.mark.asyncio @@ -678,10 +678,11 @@ async def test_templatable__string_with_non_string_output_type() -> None: @pytest.mark.asyncio async def test_templatable__with_to_exp_callable() -> None: - """When to_exp is provided, it is applied to non-template values.""" + """When to_exp is provided with output_type=None, result is lambda-wrapped.""" result = await cg.templatable(42, [], None, to_exp=lambda x: x * 2) - assert result == 84 + assert isinstance(result, cg.LambdaExpression) + assert result.capture == "" @pytest.mark.asyncio @@ -695,11 +696,12 @@ async def test_templatable__with_to_exp_callable_and_output_type() -> None: @pytest.mark.asyncio async def test_templatable__with_to_exp_dict() -> None: - """When to_exp is a dict, value is looked up.""" + """When to_exp is a dict, value is looked up and lambda-wrapped.""" mapping: dict[str, int] = {"on": 1, "off": 0} result = await cg.templatable("on", [], None, to_exp=mapping) - assert result == 1 + assert isinstance(result, cg.LambdaExpression) + assert result.capture == "" @pytest.mark.asyncio