From 04a44d6e63ee8386c3b50ccf275ada39cd50e851 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Aug 2026 16:54:33 -0500 Subject: [PATCH] Let ProgressBar own the mid-row interrupt, share the part-file naming, fold duplicate except arms --- esphome/framework_helpers.py | 32 +++++++++++++++----------------- esphome/helpers.py | 10 ++++++++++ esphome/platformio/library.py | 8 ++++++-- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/esphome/framework_helpers.py b/esphome/framework_helpers.py index d010871a04..9448fea4ac 100644 --- a/esphome/framework_helpers.py +++ b/esphome/framework_helpers.py @@ -773,11 +773,10 @@ def run_batch_downloads( try: fetch(checked) - 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 - failure = (name, err) - except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught + except (_BatchDownloadCancelled, Exception) as err: # noqa: BLE001 # pylint: disable=broad-exception-caught + # A cancelled job reports like a failure: an abandoned download + # must never read as completed if a caller sees the list after + # Ctrl-C failure = (name, err) else: return None @@ -824,12 +823,12 @@ class _BatchDownloadProgress: self._lock = threading.Lock() def tracker(self) -> Callable[[int], None]: + if self._bar is None: + return lambda _: None last = 0 def update(done: int) -> None: nonlocal last - if self._bar is None: - return with self._lock: self._sum += done - last last = done @@ -859,13 +858,7 @@ class _BatchDownloadProgress: class _EndRow(logging.Filter): def filter(self, record: logging.LogRecord) -> bool: with lock: - if ( - the_bar.last_progress is not None - and the_bar.last_progress != 100 - ): - the_bar.done() - # Force the next tick to redraw the frame - the_bar.last_progress = None + the_bar.interrupt() return True end_row = _EndRow() @@ -879,6 +872,11 @@ class _BatchDownloadProgress: handler.removeFilter(end_row) +def _part_path(dest: Path) -> Path: + """The in-progress sidecar ``download_with_resume`` streams into.""" + return dest.with_name(dest.name + ".part") + + def _cancellable_sleep( delay: float, progress: Callable[[int], None] | None, done: int ) -> None: @@ -939,7 +937,7 @@ def download_with_resume( ensure_happy_eyeballs() dest = Path(dest) - part = dest.with_name(dest.name + ".part") + part = _part_path(dest) meta = part.with_name(part.name + ".meta") dest.parent.mkdir(parents=True, exist_ok=True) last_error: Exception | None = None @@ -1062,7 +1060,7 @@ def download_with_resume( ) from last_error -def _failure_reason(e: Exception) -> str: +def _failure_reason(e: BaseException) -> str: """Format a download exception for the aggregated error message. ``requests`` appends " for url: " to HTTP errors; the URL is already @@ -1338,7 +1336,7 @@ def download_from_mirrors( if f is not None: done = f.tell() else: - part = path_target.with_name(path_target.name + ".part") + part = _part_path(path_target) done = part.stat().st_size if part.is_file() else 0 _cancellable_sleep(delay, progress, done) diff --git a/esphome/helpers.py b/esphome/helpers.py index 26e98a561d..912efebe54 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -735,6 +735,16 @@ class ProgressBar: sys.stderr.write("\n") sys.stderr.flush() + def interrupt(self) -> None: + """End a mid-row frame so the next write starts on its own row. + + The next ``update()`` redraws the bar; a finished bar stays done. + """ + if self.last_progress == 100: + return + self.done() + self.last_progress = None + def docs_url(path: str) -> str: """Return the URL to the documentation for a given path.""" diff --git a/esphome/platformio/library.py b/esphome/platformio/library.py index 537e7d1c59..7fc5c6e357 100644 --- a/esphome/platformio/library.py +++ b/esphome/platformio/library.py @@ -77,6 +77,10 @@ SRC_FILE_EXTENSIONS = list(SOURCE_KIND_FOR_SUFFIX) DOMAIN = "pio_components" +# Marks a cache dir whose archive finished extracting; a missing marker +# means a torn extraction that must be redone +_EXTRACTED_MARKER = ".esphome_extracted" + ESPHOME_DATA_KEY = "ESPHOME" ESPHOME_DATA_EXTRA_CMAKE_KEY = "EXTRA_CMAKE" @@ -118,7 +122,7 @@ class URLSource(Source): def is_cached(self, dir_suffix: str, salt: str = "", namespace: str = "") -> bool: """Whether a completed extraction already exists for this source.""" return ( - self._cache_dir(dir_suffix, salt, namespace) / ".esphome_extracted" + self._cache_dir(dir_suffix, salt, namespace) / _EXTRACTED_MARKER ).is_file() def download( @@ -135,7 +139,7 @@ class URLSource(Source): # extraction is correctly detected and re-run on the next invocation, # and lets us extract directly into ``path`` — avoiding a # post-extraction rename that races with antivirus on Windows. - extracted_marker = path / ".esphome_extracted" + extracted_marker = path / _EXTRACTED_MARKER if not extracted_marker.is_file() or force: rmdir(path, msg=f"Clean up library directory {path}")