[file] Keep resolved image paths as Path so config-hash normalizes them (#19267)

Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Christopher Pruijsen
2026-09-15 11:33:33 -05:00
committed by GitHub
co-authored by J. Nick Koston
parent 6362ae71c0
commit 457bb3ecc9
2 changed files with 54 additions and 11 deletions
+12 -10
View File
@@ -42,7 +42,7 @@ from esphome.const import (
CONF_TYPE,
CONF_URL,
)
from esphome.core import CORE, HexInt
from esphome.core import HexInt
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.external_files import RemoteFile
from esphome.types import ConfigType
@@ -76,16 +76,18 @@ def compute_local_image_path(value: str | ConfigType) -> Path:
return external_files.compute_local_file_path(DOMAIN, url)
def local_path(value: str | ConfigType) -> str:
value = value[CONF_PATH] if isinstance(value, dict) else value
return str(CORE.relative_config_path(value))
def local_path(value: Path | ConfigType) -> Path:
# cv.file_ has already resolved the path against the config dir.
return value[CONF_PATH] if isinstance(value, dict) else value
def download_file(url: str, path: Path) -> str:
def download_file(url: str, path: Path) -> Path:
# The shared NETWORK_TIMEOUT applies; a per-caller timeout would be
# silently ignored on a per-run memo hit anyway (memos key by path).
external_files.download_content(url, path)
return str(path)
# Keep the Path: config-hash normalizes Path values under the data dir,
# which a str would dump verbatim and break the CLI/add-on comparison.
return path
def _gh_svg_url_path(mdi_id: str, source: str) -> tuple[str, Path]:
@@ -93,13 +95,13 @@ def _gh_svg_url_path(mdi_id: str, source: str) -> tuple[str, Path]:
return MDI_SOURCES[source] + mdi_id + ".svg", base_dir / f"{mdi_id}.svg"
def download_gh_svg(value: str | ConfigType, source: str) -> str:
def download_gh_svg(value: str | ConfigType, source: str) -> Path:
mdi_id = value[CONF_ICON] if isinstance(value, dict) else value
url, path = _gh_svg_url_path(mdi_id, source)
return download_file(url, path)
def download_image(value: str | ConfigType) -> str:
def download_image(value: str | ConfigType) -> Path:
value = value[CONF_URL] if isinstance(value, dict) else value
return download_file(value, compute_local_image_path(value))
@@ -147,7 +149,7 @@ def _extract_entry_ref(entry: ConfigType) -> RemoteFile | None:
PREFETCH_FILES = external_files.single_stage_prefetch(_extract_entry_ref)
def validate_file_shorthand(value: Any) -> str:
def validate_file_shorthand(value: Any) -> Path:
value = cv.string_strict(value)
if (remote := _parse_remote_shorthand(value)) is not None:
return download_file(remote.url, remote.path)
@@ -165,7 +167,7 @@ LOCAL_SCHEMA = cv.All(
def mdi_schema(source: str) -> cv.All:
def validate_mdi(value: ConfigType) -> str:
def validate_mdi(value: ConfigType) -> Path:
return download_gh_svg(value, source)
return cv.All(
+42 -1
View File
@@ -5,8 +5,13 @@ from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pytest
from esphome import yaml_util
from esphome.components.file import image as file_image
from esphome.external_files import RemoteFile
from esphome.const import CONF_PATH
from esphome.core import CORE
from esphome.external_files import RemoteFile, url_cache_key
from esphome.loader import get_component, get_platform
@@ -55,6 +60,42 @@ def test_prefetch_files_yields_remote_refs(setup_core: Path) -> None:
assert files[1].url == "https://example.com/img.png"
def test_validated_file_values_hash_alike_across_data_dirs(
setup_core: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A CLI and an add-on data dir dump validated image files identically."""
url = "https://example.com/img.png"
(setup_core / "img.png").touch()
dumps: list[str] = []
for data_dir in (
setup_core / ".esphome",
setup_core.parent / f"{setup_core.name}-data",
):
monkeypatch.setenv("ESPHOME_DATA_DIR", str(data_dir))
with patch("esphome.components.file.image.external_files.download_content"):
config = {
"remote": file_image.validate_file_shorthand(url),
"mdi": file_image.validate_file_shorthand("mdi:home"),
"local": file_image.validate_file_shorthand("img.png"),
"local_schema": file_image.LOCAL_SCHEMA({CONF_PATH: "img.png"}),
}
dumps.append(
yaml_util.dump(
config,
sort_keys=True,
relative_to=CORE.config_dir,
data_dir=CORE.data_dir,
)
)
assert dumps[0] == dumps[1]
assert dumps[0].splitlines() == [
"local: img.png",
"local_schema: img.png",
"mdi: .esphome/image/mdi/home.svg",
f"remote: .esphome/image/{url_cache_key(url)}",
]
def test_extractor_matches_validator_path(setup_core: Path) -> None:
"""The path the validator downloads to equals the extractor's path."""
with patch(