diff --git a/esphome/framework_helpers.py b/esphome/framework_helpers.py index 5247619ab9..0daa446ed9 100644 --- a/esphome/framework_helpers.py +++ b/esphome/framework_helpers.py @@ -768,7 +768,13 @@ class BatchDownloadProgress: return update def done(self) -> None: - if self._bar is not None and self._bar.last_progress != 100: + # Nothing to end unless a frame was drawn and it was not the final + # one (update(1) already emitted its own newline). + if ( + self._bar is not None + and self._bar.last_progress is not None + and self._bar.last_progress != 100 + ): self._bar.done() diff --git a/tests/unit_tests/test_framework_helpers.py b/tests/unit_tests/test_framework_helpers.py index 96da38e606..098dcb7725 100644 --- a/tests/unit_tests/test_framework_helpers.py +++ b/tests/unit_tests/test_framework_helpers.py @@ -1197,6 +1197,15 @@ class TestBatchDownloadProgress: progress.done() assert stream.getvalue().endswith("50% \n") + def test_done_before_any_frame_writes_nothing(self) -> None: + """A batch aborted before any tracker fired must not emit a stray + newline for a bar that was never drawn.""" + stream = io.StringIO() + stream.isatty = lambda: True # type: ignore[method-assign] + with patch("esphome.helpers.sys.stderr", stream): + BatchDownloadProgress("Downloading", 10).done() + assert stream.getvalue() == "" + def test_done_after_full_bar_adds_nothing(self) -> None: stream = io.StringIO() stream.isatty = lambda: True # type: ignore[method-assign]