From ba0d9e77dba20f5c328e00647cf0ef40557499f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Aug 2026 16:35:48 -0500 Subject: [PATCH] Never log a blank prefetch reason, keep the download error over a tick failure Both prefetch warning loops format through _failure_reason so a message-less exception cannot log blank. A bar-frame write failure in the failure path cannot displace the download error, the failure list is annotated BaseException (cancellations are entries too), the Ctrl-C docstring notes an in-progress extraction runs to completion, and the wave-prefetch docstring carries the superseded-sibling caveat. --- esphome/espidf/framework.py | 4 +++- esphome/framework_helpers.py | 20 ++++++++++++-------- esphome/platformio/library.py | 9 +++++++-- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/esphome/espidf/framework.py b/esphome/espidf/framework.py index c65b104280..6b354d333b 100644 --- a/esphome/espidf/framework.py +++ b/esphome/espidf/framework.py @@ -17,6 +17,7 @@ import platformdirs from esphome.core import CORE, Version from esphome.framework_helpers import ( PathType, + _failure_reason, archive_extract_all, create_venv, download_from_mirrors, @@ -787,7 +788,8 @@ def _prefetch_idf_tool_archives( ], ) for name, e in failures: - _LOGGER.warning("Could not prefetch %s: %s", name, e) + # _failure_reason: a message-less exception must not log blank + _LOGGER.warning("Could not prefetch %s: %s", name, _failure_reason(e)) _LOGGER.debug("Prefetch failure detail", exc_info=e) except Exception as e: # noqa: BLE001 # pylint: disable=broad-exception-caught # The installer downloads anything missing itself; never let the diff --git a/esphome/framework_helpers.py b/esphome/framework_helpers.py index 29bd9313b4..3c30c49fd9 100644 --- a/esphome/framework_helpers.py +++ b/esphome/framework_helpers.py @@ -2,7 +2,7 @@ from collections.abc import Callable, Iterable, Iterator from concurrent.futures import ThreadPoolExecutor -from contextlib import ExitStack, contextmanager +from contextlib import ExitStack, contextmanager, suppress import hashlib import io import json @@ -746,14 +746,15 @@ def run_batch_downloads( header: str, jobs: list[tuple[str, int, Callable[[Callable[[int], None]], None]]], max_workers: int = BATCH_DOWNLOAD_WORKERS, -) -> list[tuple[str, Exception]]: +) -> list[tuple[str, BaseException]]: """Run ``(name, size, fetch)`` download jobs concurrently under one bar. Each ``fetch(tracker)`` reports absolute byte counts; the bar total is the sum of the sizes. Failures are returned after the bar is done so warnings never land on its row. Ctrl-C drops queued jobs and aborts in-flight ones at their next progress tick or backoff boundary (a - parked socket read defers that by its timeout); resumable destinations + parked socket read defers that by its timeout, and an in-progress + archive extraction runs to completion); resumable destinations (``download_with_resume``) keep their fetched ``.part`` bytes. ``jobs`` must be non-empty. """ @@ -762,7 +763,7 @@ def run_batch_downloads( def _run( name: str, fetch: Callable[[Callable[[int], None]], None] - ) -> tuple[str, Exception] | None: + ) -> tuple[str, BaseException] | None: tracker = progress.tracker() def checked(done: int) -> None: @@ -775,12 +776,15 @@ def run_batch_downloads( except _BatchDownloadCancelled as err: # Reported like a failure: an abandoned job must never read as # a completed download if a caller sees the list after Ctrl-C - tracker(0) - return (name, err) + failure = (name, err) except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught + failure = (name, err) + else: + return None + # A bar-frame write failure must not displace the download error + with suppress(Exception): tracker(0) - return (name, err) - return None + return failure ex = ThreadPoolExecutor(max_workers=max_workers) try: diff --git a/esphome/platformio/library.py b/esphome/platformio/library.py index a6bb8a1715..537e7d1c59 100644 --- a/esphome/platformio/library.py +++ b/esphome/platformio/library.py @@ -32,6 +32,7 @@ from urllib.request import url2pathname from esphome import git from esphome.core import CORE, EsphomeError, Library from esphome.framework_helpers import ( + _failure_reason, archive_extract_all, download_from_mirrors, rmdir, @@ -899,7 +900,9 @@ def _prefetch_wave( The walk's own ``download()`` stays authoritative; duplicate URLs prefetch once so two threads never share a cache directory. Archives whose size the registry did not report are left to the sequential - loop, whose per-file bars don't interleave. + loop, whose per-file bars don't interleave. A node a sibling in the + same wave supersedes has its archive fetched in vain (knowing better + would need the manifests being downloaded). """ try: components: list[ConvertedLibrary] = [] @@ -942,7 +945,9 @@ def _prefetch_wave( for name, err in failures: # The sequential call below retries and raises the real error _LOGGER.warning( - "Prefetch of %s failed (retrying sequentially): %s", name, err + "Prefetch of %s failed (retrying sequentially): %s", + name, + _failure_reason(err), ) _LOGGER.debug("Prefetch failure detail", exc_info=err) except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught