From 7629903afb64c8bfc2121e93e8b8037397f267f8 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Mon, 22 Sep 2025 06:32:59 +0200 Subject: [PATCH] [substitutions] implement !literal (#10785) --- esphome/components/substitutions/__init__.py | 4 +++- esphome/config.py | 6 +++--- esphome/yaml_util.py | 16 +++++++++++----- .../substitutions/00-simple_var.approved.yaml | 7 +++++++ .../substitutions/00-simple_var.input.yaml | 6 ++++++ 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index a96f56a045..1a1736aed1 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -4,7 +4,7 @@ from esphome import core from esphome.config_helpers import Extend, Remove, merge_config import esphome.config_validation as cv from esphome.const import CONF_SUBSTITUTIONS, VALID_SUBSTITUTIONS_CHARACTERS -from esphome.yaml_util import ESPHomeDataBase, make_data_base +from esphome.yaml_util import ESPHomeDataBase, ESPLiteralValue, make_data_base from .jinja import Jinja, JinjaStr, TemplateError, TemplateRuntimeError, has_jinja @@ -127,6 +127,8 @@ def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): def _substitute_item(substitutions, item, path, jinja, ignore_missing): + if isinstance(item, ESPLiteralValue): + return None # do not substitute inside literal blocks if isinstance(item, list): for i, it in enumerate(item): sub = _substitute_item(substitutions, it, path + [i], jinja, ignore_missing) diff --git a/esphome/config.py b/esphome/config.py index 36892fcd25..a7e47f646b 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -32,7 +32,7 @@ from esphome.log import AnsiFore, color from esphome.types import ConfigFragmentType, ConfigType from esphome.util import OrderedDict, safe_print from esphome.voluptuous_schema import ExtraKeysInvalid -from esphome.yaml_util import ESPForceValue, ESPHomeDataBase, is_secret +from esphome.yaml_util import ESPHomeDataBase, ESPLiteralValue, is_secret _LOGGER = logging.getLogger(__name__) @@ -306,7 +306,7 @@ def recursive_check_replaceme(value): return cv.Schema([recursive_check_replaceme])(value) if isinstance(value, dict): return cv.Schema({cv.valid: recursive_check_replaceme})(value) - if isinstance(value, ESPForceValue): + if isinstance(value, ESPLiteralValue): pass if isinstance(value, str) and value == "REPLACEME": raise cv.Invalid( @@ -314,7 +314,7 @@ def recursive_check_replaceme(value): "Please make sure you have replaced all fields from the sample " "configuration.\n" "If you want to use the literal REPLACEME string, " - 'please use "!force REPLACEME"' + 'please use "!literal REPLACEME"' ) return value diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index f430fa22df..359b72b48f 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -69,7 +69,7 @@ class ESPHomeDataBase: self._content_offset = database.content_offset -class ESPForceValue: +class ESPLiteralValue: pass @@ -348,9 +348,15 @@ class ESPHomeLoaderMixin: return Lambda(str(node.value)) @_add_data_ref - def construct_force(self, node: yaml.Node) -> ESPForceValue: - obj = self.construct_scalar(node) - return add_class_to_obj(obj, ESPForceValue) + def construct_literal(self, node: yaml.Node) -> ESPLiteralValue: + obj = None + if isinstance(node, yaml.ScalarNode): + obj = self.construct_scalar(node) + elif isinstance(node, yaml.SequenceNode): + obj = self.construct_sequence(node) + elif isinstance(node, yaml.MappingNode): + obj = self.construct_mapping(node) + return add_class_to_obj(obj, ESPLiteralValue) @_add_data_ref def construct_extend(self, node: yaml.Node) -> Extend: @@ -407,7 +413,7 @@ for _loader in (ESPHomeLoader, ESPHomePurePythonLoader): "!include_dir_merge_named", _loader.construct_include_dir_merge_named ) _loader.add_constructor("!lambda", _loader.construct_lambda) - _loader.add_constructor("!force", _loader.construct_force) + _loader.add_constructor("!literal", _loader.construct_literal) _loader.add_constructor("!extend", _loader.construct_extend) _loader.add_constructor("!remove", _loader.construct_remove) diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml index f5d2f8aa20..c59975b2ae 100644 --- a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml @@ -1,7 +1,11 @@ substitutions: + substituted: 99 var1: '1' var2: '2' var21: '79' + value: 33 + values: 44 + esphome: name: test test_list: @@ -19,3 +23,6 @@ test_list: - ${ undefined_var } - key1: 1 key2: 2 + - Literal $values ${are not substituted} + - ["list $value", "${is not}", "${substituted}"] + - {"$dictionary": "$value", "${is not}": "${substituted}"} diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml index 5717433c7e..3b7e7a6b4e 100644 --- a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml @@ -2,9 +2,12 @@ esphome: name: test substitutions: + substituted: 99 var1: "1" var2: "2" var21: "79" + value: 33 + values: 44 test_list: - "$var1" @@ -21,3 +24,6 @@ test_list: - ${ undefined_var } - key${var1}: 1 key${var2}: 2 + - !literal Literal $values ${are not substituted} + - !literal ["list $value", "${is not}", "${substituted}"] + - !literal {"$dictionary": "$value", "${is not}": "${substituted}"}