diff --git a/esphome/config_validation.py b/esphome/config_validation.py index ef250927f3..a62c8f2675 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -2723,6 +2723,9 @@ def rename_key( ): """Rename a config key from ``old_key`` to ``new_key``. + Specifying both keys is an error; otherwise only one of the two would + survive the rename and the other would be dropped silently. + When ``removed_in`` is set, a deprecation warning is logged if the old key is present. Pass ``component`` (the platform/component name) alongside ``removed_in`` so the warning identifies where it originates. @@ -2731,6 +2734,7 @@ def rename_key( def validator(config: dict) -> dict: config = config.copy() if old_key in config: + has_at_most_one_key(old_key, new_key)(config) if removed_in is not None: prefix = f"[{component}] " if component else "" _LOGGER.warning( diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 2ad22bc59c..864fbe6475 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -2956,6 +2956,22 @@ def test_rename_key_removed_in_with_component_prefixes_warning( ) +def test_rename_key_both_keys_rejected() -> None: + with pytest.raises(Invalid, match="Cannot specify more than one of"): + cv.rename_key("old", "new")({"old": 5, "new": 6}) + + +def test_rename_key_both_keys_rejected_with_removed_in( + caplog: pytest.LogCaptureFixture, +) -> None: + with ( + caplog.at_level(logging.WARNING, logger="esphome.config_validation"), + pytest.raises(Invalid, match="Cannot specify more than one of"), + ): + cv.rename_key("old", "new", removed_in="2026.8.0")({"old": 5, "new": 6}) + assert not caplog.records + + def test_file__existing_relative_path(setup_core: Path) -> None: (setup_core / "partitions.csv").write_text("csv\n")