[image] Only migrate recognised legacy image configs

Guard the legacy `image:` migration so it only rewrites shapes the
pre-platform schema actually accepted. A list with a non-dict or
already-platform-tagged entry, or a dict with no recognised image keys
(e.g. `image: ["bad"]`, `image: {foo: 1}`), now returns None from the
migration hook, so normal platform validation surfaces a proper error
instead of the migration silently dropping the input.
This commit is contained in:
Jesse Hills
2026-07-06 20:24:55 +12:00
parent d1938a87aa
commit 0b1e3283d5
2 changed files with 77 additions and 2 deletions
+33 -2
View File
@@ -487,6 +487,32 @@ def _is_new_image_format(config: object) -> bool:
)
def _is_legacy_image_format(config: object) -> bool:
"""True when ``config`` matches a shape the pre-platform schema accepted.
Only these shapes are migrated. Anything else -- a list containing a
non-dict (or already platform-tagged) entry, or a dict with no recognised
image keys -- is left untouched so the platform validation surfaces a
proper error instead of the migration silently dropping the input.
"""
if isinstance(config, list):
# A bare list of (not-yet-platform-tagged) image dicts.
return bool(config) and all(
isinstance(entry, dict) and CONF_PLATFORM not in entry for entry in config
)
if not isinstance(config, dict):
return False
# A single image dict, or the grouped `defaults:`/`images:`/type-key form.
return (
CONF_ID in config
or CONF_FILE in config
or any(
key in (CONF_DEFAULTS, CONF_IMAGES) or key.upper() in IMAGE_TYPE
for key in config
)
)
def _flatten_legacy_image_config(config: object) -> list[dict]:
"""Structurally flatten a legacy ``image:`` config into image dicts.
@@ -543,8 +569,13 @@ def _flatten_legacy_image_config(config: object) -> list[dict]:
def _migrate_legacy_image_config(config: object) -> list[dict] | None:
"""Rewrite a legacy ``image:`` config into the ``platform: file`` list."""
if _is_new_image_format(config):
"""Rewrite a legacy ``image:`` config into the ``platform: file`` list.
Returns None for the already-migrated platform form and for any shape the
pre-platform schema never accepted, so normal platform validation can
surface a proper error instead of the migration silently discarding input.
"""
if _is_new_image_format(config) or not _is_legacy_image_format(config):
return None
migrated = [
{CONF_PLATFORM: PLATFORM_FILE, **entry}
+44
View File
@@ -22,6 +22,7 @@ from esphome.components.image import (
CONF_TRANSPARENCY,
PLATFORM_FILE,
_flatten_legacy_image_config,
_is_legacy_image_format,
_is_new_image_format,
_migrate_legacy_image_config,
get_all_image_metadata,
@@ -323,6 +324,49 @@ def test_migrate_legacy_warns_and_prepends_platform(
assert f"platform: {PLATFORM_FILE}" in caplog.text
@pytest.mark.parametrize(
("config", "expected"),
[
# Recognised legacy shapes -> migrate.
pytest.param([{"id": "a", "file": "x.png"}], True, id="bare_list_of_dicts"),
pytest.param({"id": "a", "file": "x.png"}, True, id="single_image_dict"),
pytest.param({"file": "x.png"}, True, id="single_dict_file_only"),
pytest.param({"defaults": {}, "images": []}, True, id="defaults_images"),
pytest.param({"rgb565": [{"id": "a"}]}, True, id="type_grouped"),
# Shapes the legacy schema never accepted -> not migrated.
pytest.param([], False, id="empty_list"),
pytest.param(["bad"], False, id="list_with_non_dict"),
pytest.param([{"id": "a"}, "bad"], False, id="list_mixed_dict_and_non_dict"),
pytest.param(
[{CONF_PLATFORM: "file", "id": "a"}], False, id="already_platform_tagged"
),
pytest.param({"foo": 1}, False, id="dict_unknown_keys"),
pytest.param("a string", False, id="scalar"),
],
)
def test_is_legacy_image_format(config: object, expected: bool) -> None:
assert _is_legacy_image_format(config) is expected
@pytest.mark.parametrize(
"config",
[
pytest.param(["bad"], id="list_with_non_dict"),
pytest.param([{"id": "a"}, "bad"], id="list_mixed"),
pytest.param({"foo": 1}, id="dict_unknown_keys"),
],
)
def test_migrate_returns_none_for_invalid_legacy_shapes(
config: object, caplog: pytest.LogCaptureFixture
) -> None:
"""Unrecognised shapes are not migrated (and emit no warning) so normal
platform validation surfaces a proper error instead of silently dropping
the offending input."""
with caplog.at_level(logging.WARNING):
assert _migrate_legacy_image_config(config) is None
assert "deprecated" not in caplog.text
# --------------------------- end legacy migration --------------------------